diff --git a/.idea/dictionaries/romain.xml b/.idea/dictionaries/romain.xml
index 4f304c1..95da3c9 100644
--- a/.idea/dictionaries/romain.xml
+++ b/.idea/dictionaries/romain.xml
@@ -8,6 +8,7 @@
postgresql
socketstats
splt
+ systemd
tutux
webhooks
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 2e3acf2..5f9edb2 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -3,10 +3,17 @@
+
+
+
+
+
+
+
@@ -38,7 +45,7 @@
-
+
@@ -49,6 +56,13 @@
+
+
+
+
+
+
+
@@ -56,13 +70,6 @@
-
-
-
-
-
-
-
@@ -96,7 +103,6 @@
- 1591290805787
https://trello.com/c/vK0cBbF2/38-i18n
@@ -108,7 +114,6 @@
- 1591290805787
https://trello.com/c/SafaMBht/40-core
@@ -118,7 +123,7 @@
-
+
@@ -142,10 +147,10 @@
-
+
-
+
diff --git a/setup.cfg b/setup.cfg
index d5fe968..03cf944 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,9 +13,11 @@ install_requires =
aiohttp==3.6.2
aiosqlite==0.13.0
appdirs==1.4.4
+ astunparse==1.6.3
async-timeout==3.0.1
asyncpg==0.20.1
attrs==19.3.0
+ braceexpand==0.1.5
cachetools==4.1.0
certifi==2020.4.5.1
chardet==3.0.4
@@ -27,9 +29,11 @@ install_requires =
dnspython==1.16.0
humanize==2.4.0
idna==2.9
+ import-expression==1.1.3
ipinfo==3.0.0
ipwhois==1.1.0
iso8601==0.1.12
+ jishaku==1.18.2.188
multidict==4.7.6
psutil==5.7.0
PyPika==0.37.7
@@ -40,6 +44,7 @@ install_requires =
typing-extensions==3.7.4.2
urllib3==1.25.9
websockets==8.1
+ wheel==0.34.2
yarl==1.4.2
[options.entry_points]
diff --git a/tuxbot/__main__.py b/tuxbot/__main__.py
index d78f06b..6e03d70 100644
--- a/tuxbot/__main__.py
+++ b/tuxbot/__main__.py
@@ -16,7 +16,7 @@ from pip._vendor import distro
import tuxbot.logging
from tuxbot.core import data_manager
-from tuxbot.core.bot import Tux
+from tuxbot.core.bot import Tux, ExitCodes
from tuxbot.core.utils.functions.cli import bordered
from . import __version__
@@ -140,10 +140,10 @@ async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> NoReturn:
"""
if signal_type:
log.info("%s received. Quitting...", signal_type)
- sys.exit(0)
+ sys.exit(ExitCodes.SHUTDOWN)
elif exit_code is None:
log.info("Shutting down from unhandled exception")
- tux.shutdown_code = 1
+ tux.shutdown_code = ExitCodes.CRITICAL
if exit_code is not None:
tux.shutdown_code = exit_code
@@ -161,7 +161,7 @@ async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> NoReturn:
await asyncio.gather(*pending, return_exceptions=True)
-async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
+async def run_bot(tux: Tux, cli_flags: Namespace, loop) -> None:
"""This run the bot.
Parameters
@@ -193,13 +193,14 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
if not token:
log.critical("Token must be set if you want to login.")
- sys.exit(1)
+ sys.exit(ExitCodes.CRITICAL)
try:
+ await tux.load_packages()
await tux.start(token, bot=True)
except discord.LoginFailure:
log.critical("This token appears to be valid.")
- sys.exit(1)
+ sys.exit(ExitCodes.CRITICAL)
return None
@@ -229,7 +230,7 @@ def main() -> NoReturn:
+ "No instance provided ! "
"You can use 'tuxbot -L' to list all available instances"
+ Style.RESET_ALL)
- sys.exit(1)
+ sys.exit(ExitCodes.CRITICAL)
tux = Tux(
cli_flags=cli_flags,
@@ -237,8 +238,11 @@ def main() -> NoReturn:
dm_help=None
)
- loop.run_until_complete(run_bot(tux, cli_flags))
+ loop.run_until_complete(run_bot(tux, cli_flags, loop))
except KeyboardInterrupt:
+ print(Fore.RED
+ + "Please use quit instead of Ctrl+C to Shutdown!"
+ + Style.RESET_ALL)
log.warning("Please use quit instead of Ctrl+C to Shutdown!")
log.error("Received KeyboardInterrupt")
if tux is not None:
@@ -258,7 +262,7 @@ def main() -> NoReturn:
asyncio.set_event_loop(None)
loop.stop()
loop.close()
- exit_code = 1 if tux is None else tux.shutdown_code
+ exit_code = ExitCodes.CRITICAL if tux is None else tux.shutdown_code
sys.exit(exit_code)
diff --git a/tuxbot/cogs/__init__.py b/tuxbot/cogs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tuxbot/cogs/images/__init__.py b/tuxbot/cogs/images/__init__.py
index 516710a..514e5e4 100644
--- a/tuxbot/cogs/images/__init__.py
+++ b/tuxbot/cogs/images/__init__.py
@@ -1,5 +1,6 @@
from .images import Images
+from ...core.bot import Tux
-def setup(bot):
+def setup(bot: Tux):
bot.add_cog(Images(bot))
diff --git a/tuxbot/cogs/logs/__init__.py b/tuxbot/cogs/logs/__init__.py
index 733b020..717caff 100644
--- a/tuxbot/cogs/logs/__init__.py
+++ b/tuxbot/cogs/logs/__init__.py
@@ -3,9 +3,10 @@ import logging
from discord.ext import commands
from .logs import Logs, GatewayHandler, on_error
+from ...core.bot import Tux
-def setup(bot):
+def setup(bot: Tux):
cog = Logs(bot)
bot.add_cog(cog)
diff --git a/tuxbot/cogs/network/__init__.py b/tuxbot/cogs/network/__init__.py
index b6623f9..70fca6e 100644
--- a/tuxbot/cogs/network/__init__.py
+++ b/tuxbot/cogs/network/__init__.py
@@ -1,5 +1,6 @@
from .network import Network
+from ...core.bot import Tux
-def setup(bot):
+def setup(bot: Tux):
bot.add_cog(Network(bot))
diff --git a/tuxbot/cogs/warnings/__init__.py b/tuxbot/cogs/warnings/__init__.py
new file mode 100644
index 0000000..b49aebd
--- /dev/null
+++ b/tuxbot/cogs/warnings/__init__.py
@@ -0,0 +1,6 @@
+from .warnings import Warnings
+from ...core.bot import Tux
+
+
+def setup(bot: Tux):
+ bot.add_cog(Warnings(bot))
diff --git a/tuxbot/cogs/warnings/warnings.py b/tuxbot/cogs/warnings/warnings.py
new file mode 100644
index 0000000..cda3552
--- /dev/null
+++ b/tuxbot/cogs/warnings/warnings.py
@@ -0,0 +1,47 @@
+from typing import Union
+
+import discord
+from discord.ext import commands
+
+from tuxbot.core import checks
+from tuxbot.core.bot import Tux
+
+
+class Warnings(commands.Cog, name="Warnings"):
+ def __init__(self, bot: Tux):
+ self.bot = bot
+
+ @commands.group(name='warn', alias=['warning'])
+ @commands.guild_only()
+ @checks.is_mod()
+ async def _warn(self, ctx: commands.Context):
+ pass
+
+ @_warn.command(name="add")
+ @commands.guild_only()
+ async def _warn_add(
+ self,
+ ctx: commands.Context,
+ member: Union[discord.User, discord.Member],
+ reason: str
+ ):
+ pass
+
+ @_warn.command(name="delete", aliases=["del", "remove"])
+ @commands.guild_only()
+ async def action_del(
+ self,
+ ctx: commands.Context,
+ warn_id: int,
+ reason: str = ""
+ ):
+ pass
+
+ @_warn.command(name="list", aliases=["all"])
+ @commands.guild_only()
+ async def action_del(
+ self,
+ ctx: commands.Context,
+ member: Union[discord.User, discord.Member] = None
+ ):
+ pass
diff --git a/tuxbot/core/bot.py b/tuxbot/core/bot.py
index 579bc80..f89170d 100644
--- a/tuxbot/core/bot.py
+++ b/tuxbot/core/bot.py
@@ -1,15 +1,19 @@
+import asyncio
+import datetime
import logging
-from pathlib import Path
-from typing import List
+import sys
+from typing import List, Union
import discord
from colorama import Fore, Style, init
from discord.ext import commands
from . import Config
+from .data_manager import logs_data_path
from .utils.functions.cli import bordered
from . import __version__
+from .utils.functions.extra import ContextPlus
log = logging.getLogger("tuxbot")
init()
@@ -22,24 +26,30 @@ NAME = r"""
|_| \__,_/_/\_\_.__/ \___/ \__| |_.__/ \___/ \__|
"""
-l_extensions: List[str] = [
- "jishaku"
+packages: List[str] = [
+ "jishaku",
+ "tuxbot.cogs.warnings"
]
class Tux(commands.AutoShardedBot):
- def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
+ _loading: asyncio.Task
+
+ def __init__(self, *args, cli_flags=None, **kwargs):
# by default, if the bot shutdown without any intervention,
# it's a crash
- self.shutdown_code = 1
+ self.shutdown_code = ExitCodes.CRITICAL
self.cli_flags = cli_flags
self.instance_name = self.cli_flags.instance_name
self.last_exception = None
+ self.logs = logs_data_path(self.instance_name)
self.config = Config(self.instance_name)
async def _prefixes(bot, message) -> List[str]:
- prefixes = self.config.get_prefixes(message.guild)
+ prefixes = self.config('core').get('prefixes')
+
+ prefixes.extend(self.config.get_prefixes(message.guild))
if self.config('core').get('mentionable'):
return commands.when_mentioned_or(*prefixes)(bot, message)
@@ -51,18 +61,38 @@ class Tux(commands.AutoShardedBot):
if "owner_ids" in kwargs:
kwargs["owner_ids"] = set(kwargs["owner_ids"])
else:
- kwargs["owner_ids"] = self.config.owner_ids()
+ kwargs["owner_ids"] = self.config.owners_id()
message_cache_size = 100_000
kwargs["max_messages"] = message_cache_size
self.max_messages = message_cache_size
self.uptime = None
- self.main_dir = bot_dir
+ self._app_owners_fetched = False # to prevent abusive API calls
super().__init__(*args, help_command=None, **kwargs)
+ async def load_packages(self):
+ if packages:
+ print("Loading packages...")
+ for package in packages:
+ try:
+ self.load_extension(package)
+ except Exception as e:
+ print(Fore.RED
+ + f"Failed to load package {package}"
+ + Style.RESET_ALL
+ + f" check "
+ f"{str((self.logs / 'tuxbot.log').resolve())} "
+ f"for more details")
+
+ log.exception(
+ f"Failed to load package {package}",
+ exc_info=e
+ )
+
async def on_ready(self):
+ self.uptime = datetime.datetime.now()
INFO = {
'title': "INFO",
'rows': [
@@ -81,7 +111,7 @@ class Tux(commands.AutoShardedBot):
'title': "COGS",
'rows': []
}
- for extension in l_extensions:
+ for extension in packages:
COGS['rows'].append(
f"[{'X' if extension in self.extensions else ' '}] {extension}"
)
@@ -91,3 +121,84 @@ class Tux(commands.AutoShardedBot):
print(bordered(INFO, COGS))
print(f"\n{'=' * 118}\n\n")
+
+ async def is_owner(self, user: Union[discord.User, discord.Member]) -> bool:
+ """Determines if the user is a bot owner.
+
+ Parameters
+ ----------
+ user: Union[discord.User, discord.Member]
+
+ Returns
+ -------
+ bool
+ """
+ if user.id in self.config.owners_id():
+ return True
+
+ owner = False
+ if not self._app_owners_fetched:
+ app = await self.application_info()
+ if app.team:
+ ids = [m.id for m in app.team.members]
+ self.config.update('core', 'owners_id', ids)
+ owner = user.id in ids
+ self._app_owners_fetched = True
+
+ return owner
+
+ async def get_context(self, message: discord.Message, *, cls=None):
+ return await super().get_context(message, cls=ContextPlus)
+
+ async def process_commands(self, message: discord.Message):
+ """Check for blacklists.
+
+ """
+ if message.author.bot:
+ return
+
+ if message.guild.id in self.config.get_blacklist('guild') \
+ or message.channel.id in self.config.get_blacklist('channel') \
+ or message.author.id in self.config.get_blacklist('user'):
+ return
+
+ ctx = await self.get_context(message)
+
+ if ctx is None or ctx.valid is False:
+ self.dispatch("message_without_command", message)
+ else:
+ await self.invoke(ctx)
+
+ async def on_message(self, message: discord.Message):
+ await self.process_commands(message)
+
+ async def logout(self):
+ """Disconnect from Discord and closes all actives connections.
+
+ Todo: add postgresql logout here
+ """
+ await super().logout()
+
+ async def shutdown(self, *, restart: bool = False):
+ """Gracefully quit.
+
+ Parameters
+ ----------
+ restart:bool
+ If `True`, systemd or the launcher gonna see custom exit code
+ and reboot.
+
+ """
+ if not restart:
+ self.shutdown_code = ExitCodes.SHUTDOWN
+ else:
+ self.shutdown_code = ExitCodes.RESTART
+
+ await self.logout()
+ sys.exit(self.shutdown_code)
+
+
+class ExitCodes:
+ CRITICAL = 1
+ SHUTDOWN = 0
+ RESTART = 42
diff --git a/tuxbot/core/checks.py b/tuxbot/core/checks.py
new file mode 100644
index 0000000..8838d10
--- /dev/null
+++ b/tuxbot/core/checks.py
@@ -0,0 +1,63 @@
+from typing import Awaitable, Dict
+
+import discord
+from discord.ext import commands
+from discord.ext.commands import (
+ bot_has_permissions,
+ has_permissions,
+ is_owner,
+)
+
+from tuxbot.core.utils.functions.extra import ContextPlus
+
+__all__ = [
+ "bot_has_permissions",
+ "has_permissions",
+ "is_owner",
+ "is_mod",
+ "is_admin",
+ "check_permissions",
+ "guild_owner_or_permissions",
+]
+
+
+def is_mod():
+ async def pred(ctx):
+ if await ctx.bot.is_owner(ctx.author):
+ return True
+ permissions: discord.Permissions = ctx.channel.permissions_for(ctx.author)
+ return permissions.manage_messages
+
+ return commands.check(pred)
+
+
+def is_admin():
+ async def pred(ctx):
+ if await ctx.bot.is_owner(ctx.author):
+ return True
+ permissions: discord.Permissions = ctx.channel.permissions_for(ctx.author)
+ return permissions.administrator
+
+ return commands.check(pred)
+
+
+async def check_permissions(ctx: "ContextPlus", **perms: Dict[str, bool]):
+ if await ctx.bot.is_owner(ctx.author):
+ return True
+
+ elif not perms:
+ return False
+ resolved = ctx.channel.permissions_for(ctx.author)
+
+ return all(
+ getattr(resolved, name, None) == value for name, value in perms.items()
+ )
+
+
+def guild_owner_or_permissions(**perms: Dict[str, bool]):
+ async def pred(ctx):
+ if ctx.author is ctx.guild.owner:
+ return True
+ return await check_permissions(ctx, **perms)
+
+ return commands.check(pred)
diff --git a/tuxbot/core/config.py b/tuxbot/core/config.py
index 385cbfa..8d9a621 100644
--- a/tuxbot/core/config.py
+++ b/tuxbot/core/config.py
@@ -3,7 +3,7 @@ import logging
__all__ = ["Config"]
-from typing import List, Dict
+from typing import List, Dict, Union
import discord
@@ -39,8 +39,8 @@ class Config:
def __call__(self, item):
return self.__getitem__(item)
- def owner_ids(self) -> List[int]:
- return self.__getitem__('core').get('owner_ids')
+ def owners_id(self) -> List[int]:
+ return self.__getitem__('core').get('owners_id')
def token(self) -> str:
return self.__getitem__('core').get('token')
@@ -53,3 +53,29 @@ class Config:
.get('prefixes', [])
return prefixes
+
+ def get_blacklist(self, key: str) -> List[Union[str, int]]:
+ core = self.__getitem__('core')
+ blacklist = core \
+ .get('blacklist', {}) \
+ .get(key, [])
+
+ return blacklist
+
+ def update(self, cog_name, item, value) -> dict:
+ datas = self.__getitem__(cog_name)
+ path = data_path(self._cog_instance)
+
+ datas[item] = value
+
+ if cog_name != 'core':
+ path = path / 'cogs' / cog_name
+ else:
+ path /= 'core'
+
+ settings_file = path / 'settings.json'
+
+ with settings_file.open('w') as f:
+ json.dump(datas, f, indent=4)
+
+ return datas
diff --git a/tuxbot/core/data_manager.py b/tuxbot/core/data_manager.py
index 6aefff9..1bd4d9a 100644
--- a/tuxbot/core/data_manager.py
+++ b/tuxbot/core/data_manager.py
@@ -69,3 +69,18 @@ def cog_data_path(instance_name: str, cog_name: str) -> Path:
Generated path for cog's configs.
"""
return data_path(instance_name) / "data" / instance_name / "cogs" / cog_name
+
+
+def logs_data_path(instance_name: str) -> Path:
+ """Return Path for logs.
+
+ Parameters
+ ----------
+ instance_name:str
+
+ Returns
+ -------
+ Path
+ Generated path for logs files.
+ """
+ return data_path(instance_name) / "data" / instance_name / "logs"
diff --git a/tuxbot/core/utils/functions/extra.py b/tuxbot/core/utils/functions/extra.py
index 6783211..157fa7e 100644
--- a/tuxbot/core/utils/functions/extra.py
+++ b/tuxbot/core/utils/functions/extra.py
@@ -1,48 +1,11 @@
-import ast
import asyncio
-import json
-import os
import discord
from discord.ext import commands, flags
-from configs.bot.protected import protected
-from configs.bot.settings import prefixes
-
class ContextPlus(commands.Context):
async def send(self, content=None, *args, **kwargs):
- if content is not None:
- for value in protected:
- content = content.replace(
- str(value),
- '[Deleted]'
- )
-
- if kwargs.get('content') is not None:
- for value in protected:
- kwargs['content'] = kwargs['content'].replace(
- str(value),
- '[Deleted]'
- )
-
- if kwargs.get('embeds') is not None and len(kwargs.get('embeds')) > 0:
- for i, embed in enumerate(kwargs.get('embeds')):
- embed = str(kwargs.get('embed').to_dict())
- for value in protected:
- embed = embed.replace(str(value), '[Deleted]')
- kwargs['embeds'][i] = discord.Embed.from_dict(
- ast.literal_eval(embed)
- )
-
- if kwargs.get('embed') is not None:
- embed = str(kwargs.get('embed').to_dict())
- for value in protected:
- embed = embed.replace(str(value), '[Deleted]')
- kwargs['embed'] = discord.Embed.from_dict(
- ast.literal_eval(embed)
- )
-
if (hasattr(self.command, 'deletable') and self.command.deletable) \
and kwargs.pop('deletable', True):
message = await super().send(content, *args, **kwargs)
@@ -86,29 +49,3 @@ class GroupPlus(flags.FlagGroup):
def group_extra(*args, **kwargs):
return commands.group(*args, **kwargs, cls=GroupPlus)
-
-
-async def get_prefix(bot, message):
- custom_prefix = prefixes
- if message.guild:
- path = f"configs/guilds/{str(message.guild.id)}.json"
-
- if os.path.exists(path):
- with open(path) as f:
- datas = json.load(f)
-
- custom_prefix = datas["Prefix"]
-
- return commands.when_mentioned_or(*custom_prefix)(bot, message)
-
-
-def get_owners() -> list:
- with open("configs/bot/whitelist.json") as f:
- datas = json.load(f)
-
- return datas['owners']
-
-
-def get_blacklist() -> dict:
- with open("configs/bot/blacklist.json") as f:
- return json.load(f)
diff --git a/tuxbot/logging.py b/tuxbot/logging.py
index 8a9b381..2013c0f 100644
--- a/tuxbot/logging.py
+++ b/tuxbot/logging.py
@@ -17,6 +17,7 @@ def init_logging(level: int, location: pathlib.Path) -> None:
location:Path
Where to store logs.
"""
+
dpy_logger = logging.getLogger("discord")
dpy_logger.setLevel(logging.WARN)
dpy_logger_file = location / 'discord.log'
@@ -39,10 +40,7 @@ def init_logging(level: int, location: pathlib.Path) -> None:
maxBytes=MAX_BYTES, backupCount=MAX_OLD_LOGS
)
- dpy_logger.addHandler(dpy_handler)
- base_logger.addHandler(base_handler)
-
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
- base_logger.addHandler(stdout_handler)
- dpy_logger.addHandler(stdout_handler)
+ dpy_logger.addHandler(dpy_handler)
+ base_logger.addHandler(base_handler)
diff --git a/tuxbot/setup.py b/tuxbot/setup.py
index b8fa076..280da2d 100644
--- a/tuxbot/setup.py
+++ b/tuxbot/setup.py
@@ -292,6 +292,7 @@ def finish_setup(data_dir: Path) -> NoReturn:
'prefixes': prefixes,
'mentionable': mentionable,
'owners_id': owners_id,
+ 'locale': "en-US"
}
with core_file.open("w") as fs: