tldr: core, warn's skeleton

This commit is contained in:
Romain J 2020-06-05 00:29:14 +02:00
parent b5b7f0c7ef
commit 815709d68b
17 changed files with 328 additions and 106 deletions

View file

@ -8,6 +8,7 @@
<w>postgresql</w>
<w>socketstats</w>
<w>splt</w>
<w>systemd</w>
<w>tutux</w>
<w>webhooks</w>
</words>

View file

@ -3,10 +3,17 @@
<component name="ChangeListManager">
<list default="true" id="c97c8a30-7573-4dcd-a0d4-5bf94b8ddbbd" name="5ed57ed9960f35191182a924 core" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/setup.cfg" beforeDir="false" afterPath="$PROJECT_DIR$/setup.cfg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/__main__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/__main__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/cogs/images/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/cogs/images/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/cogs/logs/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/cogs/logs/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/cogs/network/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/cogs/network/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/core/bot.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/core/bot.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/core/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/core/config.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/core/data_manager.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/core/data_manager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/core/utils/functions/extra.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/core/utils/functions/extra.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/logging.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/logging.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tuxbot/setup.py" beforeDir="false" afterPath="$PROJECT_DIR$/tuxbot/setup.py" afterDir="false" />
</list>
<list id="a3abf5c0-7587-46e4-8f09-88e34a1ab8a4" name="5ed41911b012e33f68a07e7a i18n" comment="" />
<list id="6566fca1-2e90-48bb-9e74-dd3badbaca99" name="Default Changelist" comment="" />
@ -38,7 +45,7 @@
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/tuxbot" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/tuxbot/core" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
@ -49,6 +56,13 @@
<property name="tasks.open.task.update.state.enabled" value="false" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/tuxbot/core" />
<recent name="$PROJECT_DIR$/tuxbot/cogs" />
<recent name="$PROJECT_DIR$/tuxbot" />
<recent name="$PROJECT_DIR$/tuxbot/cogs/network" />
<recent name="$PROJECT_DIR$" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/tuxbot/cogs/network" />
<recent name="$PROJECT_DIR$/tuxbot/cogs/logs" />
@ -56,13 +70,6 @@
<recent name="$PROJECT_DIR$/tuxbot/cogs" />
<recent name="$PROJECT_DIR$/tuxbot/core" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/tuxbot" />
<recent name="$PROJECT_DIR$/tuxbot/cogs/network" />
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/tuxbot/cogs" />
<recent name="$PROJECT_DIR$/utils/locales" />
</key>
</component>
<component name="SvnConfiguration">
<configuration />
@ -96,7 +103,6 @@
</task>
<task id="5ed41911b012e33f68a07e7a" summary="i18n">
<changelist id="a3abf5c0-7587-46e4-8f09-88e34a1ab8a4" name="5ed41911b012e33f68a07e7a i18n" comment="" />
<created>1591290805787</created>
<option name="issue" value="true" />
<url>https://trello.com/c/vK0cBbF2/38-i18n</url>
<option name="number" value="38" />
@ -108,7 +114,6 @@
</task>
<task active="true" id="5ed57ed9960f35191182a924" summary="core">
<changelist id="c97c8a30-7573-4dcd-a0d4-5bf94b8ddbbd" name="5ed57ed9960f35191182a924 core" comment="" />
<created>1591290805787</created>
<option name="issue" value="true" />
<url>https://trello.com/c/SafaMBht/40-core</url>
<option name="number" value="40" />
@ -118,7 +123,7 @@
<workItem from="1591054878071" duration="1039000" />
<workItem from="1591088657371" duration="4107000" />
<workItem from="1591128560850" duration="40267000" />
<workItem from="1591281151234" duration="9745000" />
<workItem from="1591281151234" duration="23336000" />
</task>
<option name="localTasksCounter" value="2" />
<option name="createBranch" value="false" />
@ -142,10 +147,10 @@
<option name="version" value="2" />
</component>
<component name="WindowStateProjectService">
<state x="2338" y="213" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1589991158766">
<state x="2338" y="213" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1591298885935">
<screen x="1920" y="0" width="1920" height="1080" />
</state>
<state x="2338" y="213" key="#com.intellij.execution.impl.EditConfigurationsDialog/1920.0.1920.1080/0.29.1920.1051@1920.0.1920.1080" timestamp="1589991158766" />
<state x="2338" y="213" key="#com.intellij.execution.impl.EditConfigurationsDialog/1920.0.1920.1080/0.29.1920.1051@1920.0.1920.1080" timestamp="1591298885935" />
<state x="2616" y="357" width="521" height="396" key="#com.intellij.fileTypes.FileTypeChooser" timestamp="1589928148179">
<screen x="1920" y="0" width="1920" height="1080" />
</state>

View file

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

View file

@ -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 <prefix>quit instead of Ctrl+C to Shutdown!"
+ Style.RESET_ALL)
log.warning("Please use <prefix>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)

0
tuxbot/cogs/__init__.py Normal file
View file

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
from .warnings import Warnings
from ...core.bot import Tux
def setup(bot: Tux):
bot.add_cog(Warnings(bot))

View file

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

View file

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

63
tuxbot/core/checks.py Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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