diff --git a/.idea/dictionaries/romain.xml b/.idea/dictionaries/romain.xml
index 3108911..b0a64d1 100644
--- a/.idea/dictionaries/romain.xml
+++ b/.idea/dictionaries/romain.xml
@@ -17,6 +17,7 @@
gnous
ipinfo
iplocalise
+ ipwhois
jishaku
langue
levelname
diff --git a/setup.cfg b/setup.cfg
index 223b283..f5f62b5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,6 +21,7 @@ install_requires =
discord.py @ git+https://github.com/Rapptz/discord.py
discord_flags>=2.1.1
humanize>=2.6.0
+ ipwhois>=1.2.0
jishaku>=1.19.1.200
psutil>=5.7.2
requests>=2.25.1
diff --git a/tuxbot/cogs/Admin/locales/messages.pot b/tuxbot/cogs/Admin/locales/messages.pot
index e43ef9c..c254abd 100644
--- a/tuxbot/cogs/Admin/locales/messages.pot
+++ b/tuxbot/cogs/Admin/locales/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
-"POT-Creation-Date: 2021-01-25 14:36+0100\n"
+"POT-Creation-Date: 2021-01-25 16:09+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
diff --git a/tuxbot/cogs/Custom/locales/messages.pot b/tuxbot/cogs/Custom/locales/messages.pot
index f83bcd3..c5523a8 100644
--- a/tuxbot/cogs/Custom/locales/messages.pot
+++ b/tuxbot/cogs/Custom/locales/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
-"POT-Creation-Date: 2021-01-25 14:36+0100\n"
+"POT-Creation-Date: 2021-01-25 16:09+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
diff --git a/tuxbot/cogs/Network/__init__.py b/tuxbot/cogs/Network/__init__.py
new file mode 100644
index 0000000..2846ead
--- /dev/null
+++ b/tuxbot/cogs/Network/__init__.py
@@ -0,0 +1,19 @@
+from collections import namedtuple
+
+from tuxbot.core.bot import Tux
+from .network import Network
+from .config import NetworkConfig, HAS_MODELS
+
+VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
+version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
+
+__version__ = "v{}.{}.{}-{}".format(
+ version_info.major,
+ version_info.minor,
+ version_info.micro,
+ version_info.release_level,
+).replace("\n", "")
+
+
+def setup(bot: Tux):
+ bot.add_cog(Network(bot))
diff --git a/tuxbot/cogs/Network/config.py b/tuxbot/cogs/Network/config.py
new file mode 100644
index 0000000..51276d6
--- /dev/null
+++ b/tuxbot/cogs/Network/config.py
@@ -0,0 +1,15 @@
+from structured_config import Structure, StrField
+
+HAS_MODELS = False
+
+
+class NetworkConfig(Structure):
+ ipinfoKey: str = StrField("")
+
+
+extra = {
+ "ipinfoKey": {
+ "type": str,
+ "description": "API Key for ipinfo.io (.iplocalise command)",
+ },
+}
diff --git a/tuxbot/cogs/Network/functions/converters.py b/tuxbot/cogs/Network/functions/converters.py
new file mode 100644
index 0000000..8d02e17
--- /dev/null
+++ b/tuxbot/cogs/Network/functions/converters.py
@@ -0,0 +1,39 @@
+import re
+
+from discord.ext import commands
+
+from tuxbot.cogs.Network.functions.exceptions import InvalidIp
+
+
+def _(x):
+ return x
+
+
+DOMAIN_PATTERN = "^([A-Za-z0-9]\.|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]\.){1,3}[A-Za-z]{2,6}$"
+IP_PATTERN = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
+
+
+class IPConverter(commands.Converter):
+ async def convert(self, ctx, argument):
+ argument = argument.replace("http://", "").replace("https://", "")
+
+ check_domain = re.match(DOMAIN_PATTERN, argument)
+ check_ip = re.match(IP_PATTERN, argument)
+
+ if check_domain or check_ip:
+ return argument
+
+ raise InvalidIp(_("Invalid ip or domain"))
+
+
+class IPVersionConverter(commands.Converter):
+ async def convert(self, ctx, argument):
+ if not argument:
+ return argument
+
+ argument = argument.replace("-", "").replace("p", "").replace("v", "")
+
+ if argument not in ["4", "6"]:
+ raise InvalidIp(_("Invalid ip version"))
+
+ return argument
diff --git a/tuxbot/cogs/Network/functions/exceptions.py b/tuxbot/cogs/Network/functions/exceptions.py
new file mode 100644
index 0000000..9786340
--- /dev/null
+++ b/tuxbot/cogs/Network/functions/exceptions.py
@@ -0,0 +1,9 @@
+from discord.ext import commands
+
+
+class RFC18(commands.UserNotFound):
+ pass
+
+
+class InvalidIp(commands.BadArgument):
+ pass
diff --git a/tuxbot/cogs/Network/locales/en-US.po b/tuxbot/cogs/Network/locales/en-US.po
new file mode 100644
index 0000000..1b37c06
--- /dev/null
+++ b/tuxbot/cogs/Network/locales/en-US.po
@@ -0,0 +1,64 @@
+# French translations for Tuxbot-bot package
+# Traductions françaises du paquet Tuxbot-bot.
+# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the Tuxbot-bot package.
+# Automatically generated, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Tuxbot-bot\n"
+"Report-Msgid-Bugs-To: rick@gnous.eu\n"
+"POT-Creation-Date: 2021-01-19 14:39+0100\n"
+"PO-Revision-Date: 2021-01-19 14:39+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: tuxbot/cogs/Network/functions/converters.py:22
+msgid "Invalid ip or domain"
+msgstr ""
+
+#: tuxbot/cogs/Network/functions/converters.py:35
+msgid "Invalid ip version"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:49 tuxbot/cogs/Network/network.py:52
+#, python-brace-format
+msgid "in v{v}"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:61
+#, python-brace-format
+msgid "Impossible to collect information on this ip {version}"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:89
+#, python-brace-format
+msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:109
+msgid "*Retrieving information...*"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:123
+#, python-brace-format
+msgid "Information for ``{ip} ({ip_address})``"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:135 tuxbot/cogs/Network/network.py:156
+msgid "Belongs to:"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:140 tuxbot/cogs/Network/network.py:161
+msgid "Region:"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:174
+#, python-brace-format
+msgid "Hostname: {hostname}"
+msgstr ""
diff --git a/tuxbot/cogs/Network/locales/fr-FR.po b/tuxbot/cogs/Network/locales/fr-FR.po
new file mode 100644
index 0000000..61b432a
--- /dev/null
+++ b/tuxbot/cogs/Network/locales/fr-FR.po
@@ -0,0 +1,64 @@
+# French translations for Tuxbot-bot package
+# Traductions françaises du paquet Tuxbot-bot.
+# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the Tuxbot-bot package.
+# Automatically generated, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Tuxbot-bot\n"
+"Report-Msgid-Bugs-To: rick@gnous.eu\n"
+"POT-Creation-Date: 2021-01-19 14:39+0100\n"
+"PO-Revision-Date: 2021-01-19 14:39+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: tuxbot/cogs/Network/functions/converters.py:22
+msgid "Invalid ip or domain"
+msgstr "Nome de domaine ou adresse IP invalide"
+
+#: tuxbot/cogs/Network/functions/converters.py:35
+msgid "Invalid ip version"
+msgstr "Version d'adresse IP invalide"
+
+#: tuxbot/cogs/Network/network.py:49 tuxbot/cogs/Network/network.py:52
+#, python-brace-format
+msgid "in v{v}"
+msgstr "en v{v}"
+
+#: tuxbot/cogs/Network/network.py:61
+#, python-brace-format
+msgid "Impossible to collect information on this ip {version}"
+msgstr "Impossible de collecter des informations pour cette IP {version}"
+
+#: tuxbot/cogs/Network/network.py:89
+#, python-brace-format
+msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
+msgstr "L'adresse ip {ip_address} est est reservée à un usage local selon la RFC 1918"
+
+#: tuxbot/cogs/Network/network.py:109
+msgid "*Retrieving information...*"
+msgstr "*Récupération des informations...*"
+
+#: tuxbot/cogs/Network/network.py:123
+#, python-brace-format
+msgid "Information for ``{ip} ({ip_address})``"
+msgstr "Informations pour ``{ip} ({ip_address})``"
+
+#: tuxbot/cogs/Network/network.py:135 tuxbot/cogs/Network/network.py:156
+msgid "Belongs to:"
+msgstr "Appartient à :"
+
+#: tuxbot/cogs/Network/network.py:140 tuxbot/cogs/Network/network.py:161
+msgid "Region:"
+msgstr "Région :"
+
+#: tuxbot/cogs/Network/network.py:174
+#, python-brace-format
+msgid "Hostname: {hostname}"
+msgstr "Nom d'hôte : {hostname}"
diff --git a/tuxbot/cogs/Network/locales/messages.pot b/tuxbot/cogs/Network/locales/messages.pot
new file mode 100644
index 0000000..3c60d0a
--- /dev/null
+++ b/tuxbot/cogs/Network/locales/messages.pot
@@ -0,0 +1,63 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the Tuxbot-bot package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Tuxbot-bot\n"
+"Report-Msgid-Bugs-To: rick@gnous.eu\n"
+"POT-Creation-Date: 2021-01-25 16:09+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: tuxbot/cogs/Network/functions/converters.py:22
+msgid "Invalid ip or domain"
+msgstr ""
+
+#: tuxbot/cogs/Network/functions/converters.py:35
+msgid "Invalid ip version"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:49 tuxbot/cogs/Network/network.py:52
+#, python-brace-format
+msgid "in v{v}"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:61
+#, python-brace-format
+msgid "Impossible to collect information on this ip {version}"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:89
+#, python-brace-format
+msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:109
+msgid "*Retrieving information...*"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:123
+#, python-brace-format
+msgid "Information for ``{ip} ({ip_address})``"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:135 tuxbot/cogs/Network/network.py:156
+msgid "Belongs to:"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:140 tuxbot/cogs/Network/network.py:161
+msgid "Region:"
+msgstr ""
+
+#: tuxbot/cogs/Network/network.py:174
+#, python-brace-format
+msgid "Hostname: {hostname}"
+msgstr ""
diff --git a/tuxbot/cogs/Network/models/__init__.py b/tuxbot/cogs/Network/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tuxbot/cogs/Network/network.py b/tuxbot/cogs/Network/network.py
new file mode 100644
index 0000000..9455c5f
--- /dev/null
+++ b/tuxbot/cogs/Network/network.py
@@ -0,0 +1,199 @@
+import functools
+import logging
+import socket
+from typing import Union, NoReturn
+
+import discord
+import ipinfo
+import ipwhois
+from discord.ext import commands
+from ipinfo.exceptions import RequestQuotaExceededError
+from ipwhois import Net
+from ipwhois.asn import IPASN
+from structured_config import ConfigFile
+
+from tuxbot.cogs.Network.functions.converters import (
+ IPConverter,
+ IPVersionConverter,
+)
+from tuxbot.cogs.Network.functions.exceptions import RFC18, InvalidIp
+from tuxbot.core.bot import Tux
+from tuxbot.core.i18n import (
+ Translator,
+)
+from tuxbot.core.utils.data_manager import cogs_data_path
+from tuxbot.core.utils.functions.extra import (
+ ContextPlus,
+ command_extra,
+)
+from .config import NetworkConfig
+
+log = logging.getLogger("tuxbot.cogs.Network")
+_ = Translator("Network", __file__)
+
+
+class Network(commands.Cog, name="Network"):
+ def __init__(self, bot: Tux):
+ self.bot = bot
+ self.config: NetworkConfig = ConfigFile(
+ str(
+ cogs_data_path(self.bot.instance_name, "Network")
+ / "config.yaml"
+ ),
+ NetworkConfig,
+ ).config
+
+ async def cog_command_error(self, ctx, error):
+ if isinstance(error, (RequestQuotaExceededError, RFC18, InvalidIp)):
+ await ctx.send(_(str(error), ctx, self.bot.config))
+
+ # =========================================================================
+ # =========================================================================
+
+ async def _get_ip(self, ctx: ContextPlus, ip: str, inet: str = "") -> str:
+ inet_text = ""
+
+ if inet == "6":
+ inet = socket.AF_INET6
+ inet_text = _("in v{v}", ctx, self.bot.config).format(v=inet)
+ elif inet == "4":
+ inet = socket.AF_INET
+ inet_text = _("in v{v}", ctx, self.bot.config).format(v=inet)
+ else:
+ inet = 0
+
+ try:
+ return socket.getaddrinfo(str(ip), None, inet)[1][4][0]
+ except socket.gaierror:
+ return await ctx.send(
+ _(
+ "Impossible to collect information on this ip {version}".format(
+ version=inet_text
+ ),
+ ctx,
+ self.bot.config,
+ )
+ )
+
+ @staticmethod
+ def _get_hostname(ip: str) -> str:
+ try:
+ return socket.gethostbyaddr(ip)[0]
+ except socket.herror:
+ return "N/A"
+
+ @staticmethod
+ def get_ipwhois_result(ip_address: str) -> Union[NoReturn, dict]:
+ try:
+ net = Net(ip_address)
+ obj = IPASN(net)
+ return obj.lookup()
+ except ipwhois.exceptions.ASNRegistryError:
+ return {}
+ except ipwhois.exceptions.IPDefinedError as e:
+
+ def _(x):
+ return x
+
+ raise RFC18(
+ _(
+ "IP address {ip_address} is already defined as Private-Use"
+ " Networks via RFC 1918."
+ )
+ ) from e
+
+ async def get_ipinfo_result(
+ self, ip_address: str
+ ) -> Union[NoReturn, dict]:
+ try:
+ handler = ipinfo.getHandlerAsync(self.config.ipinfoKey)
+ return (await handler.getDetails(ip_address)).all
+ except RequestQuotaExceededError:
+ return {}
+
+ # =========================================================================
+ # =========================================================================
+
+ @command_extra(name="iplocalise", aliases=["localiseip"], deletable=True)
+ async def _iplocalise(
+ self,
+ ctx: ContextPlus,
+ ip: IPConverter,
+ version: IPVersionConverter = "",
+ ):
+ tmp = await ctx.send(
+ _("*Retrieving information...*", ctx, self.bot.config),
+ deletable=False,
+ )
+
+ ip_address = await self._get_ip(ctx, str(ip), str(version))
+ ip_hostname = self._get_hostname(ip_address)
+
+ ipinfo_result = await self.get_ipinfo_result(ip_address)
+ ipwhois_result = await self.bot.loop.run_in_executor(
+ None, functools.partial(self.get_ipwhois_result, ip_address)
+ )
+
+ e = discord.Embed(
+ title=_(
+ "Information for ``{ip} ({ip_address})``", ctx, self.bot.config
+ ).format(ip=ip, ip_address=ip_address),
+ color=0x5858D7,
+ )
+
+ if ipinfo_result:
+ org = ipinfo_result.get("org", "")
+ asn = org.split()[0]
+
+ e.add_field(
+ name=_("Belongs to:", ctx, self.bot.config),
+ value=f"[{org}](https://bgp.he.net/{asn})",
+ inline=True,
+ )
+
+ e.add_field(
+ name=_("Region:", ctx, self.bot.config),
+ value=f"{ipinfo_result.get('city', 'N/A')} - "
+ f"{ipinfo_result.get('region', 'N/A')} "
+ f"({ipinfo_result.get('country', 'N/A')})",
+ inline=False,
+ )
+
+ e.set_thumbnail(
+ url=f"https://www.countryflags.io/{ipinfo_result['country']}"
+ f"/shiny/64.png"
+ )
+ elif ipwhois_result:
+ org = ipwhois_result.get("asn_description", "N/A")
+ asn = ipwhois_result.get("asn", "N/A")
+ asn_country = ipwhois_result.get("asn_country_code", "N/A")
+
+ e.add_field(
+ name=_("Belongs to:", ctx, self.bot.config),
+ value=f"{org} ([AS{asn}](https://bgp.he.net/{asn}))",
+ inline=True,
+ )
+
+ e.add_field(
+ name=_("Region:", ctx, self.bot.config),
+ value=asn_country,
+ inline=False,
+ )
+
+ e.set_thumbnail(
+ url=f"https://www.countryflags.io/{asn_country}/shiny/64.png"
+ )
+
+ if ipwhois_result:
+ e.add_field(
+ name="RIR :", value=ipwhois_result["asn_registry"], inline=True
+ )
+
+ e.set_footer(
+ text=_("Hostname: {hostname}", ctx, self.bot.config).format(
+ hostname=ip_hostname
+ ),
+ )
+
+ await tmp.delete()
+ await ctx.send(embed=e)
diff --git a/tuxbot/cogs/Polls/locales/messages.pot b/tuxbot/cogs/Polls/locales/messages.pot
index 12e214d..1d21de4 100644
--- a/tuxbot/cogs/Polls/locales/messages.pot
+++ b/tuxbot/cogs/Polls/locales/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
-"POT-Creation-Date: 2021-01-25 14:36+0100\n"
+"POT-Creation-Date: 2021-01-25 16:09+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
diff --git a/tuxbot/cogs/Utils/locales/messages.pot b/tuxbot/cogs/Utils/locales/messages.pot
index 58f747e..ff04ab9 100644
--- a/tuxbot/cogs/Utils/locales/messages.pot
+++ b/tuxbot/cogs/Utils/locales/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
-"POT-Creation-Date: 2021-01-25 14:36+0100\n"
+"POT-Creation-Date: 2021-01-25 16:09+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -29,7 +29,7 @@ msgstr ""
msgid ""
"**{}** physical memory\n"
"**{}** virtual memory\n"
-"**{}**% CPU"
+"**{:.2f}**% CPU"
msgstr ""
#: tuxbot/cogs/Utils/utils.py:77
diff --git a/tuxbot/core/utils/functions/extra.py b/tuxbot/core/utils/functions/extra.py
index aacca0d..05b8fe4 100644
--- a/tuxbot/core/utils/functions/extra.py
+++ b/tuxbot/core/utils/functions/extra.py
@@ -25,8 +25,9 @@ class ContextPlus(commands.Context):
delete_after=None,
nonce=None,
allowed_mentions=None,
- deletable=False
+ deletable=True
): # i know *args and **kwargs but, i prefer work with same values
+
if content:
content = content.replace(
self.bot.config.Core.token, TOKEN_REPLACEMENT
@@ -47,7 +48,7 @@ class ContextPlus(commands.Context):
if (
hasattr(self.command, "deletable") and self.command.deletable
- ) or deletable:
+ ) and deletable:
message = await super().send(
content=content,
tts=tts,
diff --git a/tuxbot/core/utils/functions/utils.py b/tuxbot/core/utils/functions/utils.py
index 6ed928b..5268612 100644
--- a/tuxbot/core/utils/functions/utils.py
+++ b/tuxbot/core/utils/functions/utils.py
@@ -1,2 +1,24 @@
+import functools
+
+from discord.ext import commands
+
+from tuxbot.core.utils.functions.extra import ContextPlus
+
+
def upper_first(string: str) -> str:
return "".join(string[0].upper() + string[1:])
+
+
+def typing(func):
+ @functools.wraps(func)
+ async def wrapped(*args, **kwargs):
+ context = (
+ args[0]
+ if isinstance(args[0], (commands.Context, ContextPlus))
+ else args[1]
+ )
+
+ async with context.typing():
+ await func(*args, **kwargs)
+
+ return wrapped