From 8143d59c7674fbc6e838a0b52ee8966faa23a336 Mon Sep 17 00:00:00 2001 From: flifloo Date: Wed, 8 Apr 2020 17:11:23 +0200 Subject: [PATCH] Init commit --- .gitignore | 138 +++++++++++++++++++++++++++++++++++ LICENSE | 21 ++++++ README.md | 2 + bot_bde/__init__.py | 8 +++ bot_bde/config.py | 20 ++++++ bot_bde/logger.py | 20 ++++++ config_exemple.json | 1 + extensions/__init__.py | 4 ++ extensions/help.py | 32 +++++++++ extensions/speak.py | 158 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 404 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bot_bde/__init__.py create mode 100644 bot_bde/config.py create mode 100644 bot_bde/logger.py create mode 100644 config_exemple.json create mode 100644 extensions/__init__.py create mode 100644 extensions/help.py create mode 100644 extensions/speak.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..106b040 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Bot configuration +config.json + +# JetBrains Stuff +.idea + +# Bot logs +logs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70c5f19 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ethanell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebe0038 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# BotBDE +A discord bot for my BDE server diff --git a/bot_bde/__init__.py b/bot_bde/__init__.py new file mode 100644 index 0000000..3aed77b --- /dev/null +++ b/bot_bde/__init__.py @@ -0,0 +1,8 @@ +from bot_bde.config import config +from discord.ext import commands + +bot = commands.Bot(command_prefix=config.get("prefix")) + +import extensions + +bot.run(config.get("token")) diff --git a/bot_bde/config.py b/bot_bde/config.py new file mode 100644 index 0000000..22554f0 --- /dev/null +++ b/bot_bde/config.py @@ -0,0 +1,20 @@ +from bot_bde.logger import logger +from os.path import isfile +from json import load + +logger = logger.getChild("Config") + +if not isfile("config.json"): + logger.critical("Config file not found !") + exit(1) + +config = {} +with open("config.json") as conf: + logger.info("Loading configuration") + try: + config.update(load(conf)) + except Exception as e: + logger.critical(f"Fail to load configuration: {e}") + exit(1) + else: + logger.info("Configuration load successful") diff --git a/bot_bde/logger.py b/bot_bde/logger.py new file mode 100644 index 0000000..4e586c7 --- /dev/null +++ b/bot_bde/logger.py @@ -0,0 +1,20 @@ +import logging +from logging import handlers +from os import mkdir +from os.path import isdir + +if not isdir("../logs"): + mkdir("../logs") + +log_format = "{%(levelname)s}[%(asctime)s]: %(name)s | %(message)s" + +logging.basicConfig( + format=log_format, + level=logging.INFO +) +logger = logging.getLogger("BotBDE") +handler = handlers.TimedRotatingFileHandler("../logs/current.log", when="d", interval=1) +handler.suffix = "%Y-%m-%d" +handler.style = log_format +handler.setFormatter(logging.Formatter(log_format)) +logger.addHandler(handler) diff --git a/config_exemple.json b/config_exemple.json new file mode 100644 index 0000000..b49a4a0 --- /dev/null +++ b/config_exemple.json @@ -0,0 +1 @@ +{"prefix": "!", "token": "GOOD_BOT_TOKEN", "admin_id": "GOOD_USER_ID"} \ No newline at end of file diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 0000000..61f74b9 --- /dev/null +++ b/extensions/__init__.py @@ -0,0 +1,4 @@ +from bot_bde import bot + +bot.load_extension("extensions.help") +bot.load_extension("extensions.speak") diff --git a/extensions/help.py b/extensions/help.py new file mode 100644 index 0000000..6a09117 --- /dev/null +++ b/extensions/help.py @@ -0,0 +1,32 @@ +from discord.ext import commands +from bot_bde.logger import logger + + +extension_name = "help" +logger = logger.getChild(extension_name) + + +@commands.command("help") +async def help_cmd(ctx): + await ctx.send("Help !") + + +def setup(bot): + logger.info(f"Loading...") + try: + bot.help_command = None + bot.add_command(help_cmd) + except Exception as e: + logger.error(f"Error loading: {e}") + else: + logger.info(f"Load successful") + + +def teardown(bot): + logger.info(f"Unloading...") + try: + bot.remove_command("help") + except Exception as e: + logger.error(f"Error unloading: {e}") + else: + logger.info(f"Unload successful") diff --git a/extensions/speak.py b/extensions/speak.py new file mode 100644 index 0000000..bac23ec --- /dev/null +++ b/extensions/speak.py @@ -0,0 +1,158 @@ +from discord.ext import commands +from discord import Member, VoiceState, Embed +from bot_bde.logger import logger + + +extension_name = "speak" +logger = logger.getChild(extension_name) + + +class Speak(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.strict = False + self.voice_chan = None + self.waiting = [] + self.lastSpeaker = None + + @commands.group("speak", pass_context=True) + async def speak(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + if ctx.author.voice is None or ctx.author.voice.channel is None: + await ctx.send("Your not in a voice channel !") + elif self.voice_chan is None: + await ctx.send("Voice channel not set !") + elif ctx.author.voice.channel.id != self.voice_chan: + await ctx.send("Your not in the good voice channel !") + elif ctx.author.id in self.waiting: + await ctx.message.add_reaction("\u274C") + else: + self.waiting.append(ctx.author.id) + await ctx.message.add_reaction("\U0001f44d") + + @speak.group("remove", pass_context=True) + async def speak_remove(self, ctx: commands.Context): + if len(ctx.message.mentions) != 0: + if self.voice_chan and ctx.guild.get_channel(self.voice_chan).permissions_for(ctx.author).mute_members: + for speaker in ctx.message.mentions: + self.waiting.remove(speaker.id) + await ctx.message.add_reaction("\U0001f44d") + else: + await ctx.message.add_reaction("\u274C") + elif ctx.author.id in self.waiting: + self.waiting.remove(ctx.author.id) + await ctx.message.add_reaction("\U0001f44d") + else: + await ctx.message.add_reaction("\u274C") + + @speak.group("list", pass_context=True) + async def speak_list(self, ctx: commands.Context): + if ctx.author.voice.channel is None or not self.voice_chan: + await ctx.message.add_reaction("\u274C") + else: + embed = Embed(title="Waiting list") + for i, speaker in enumerate(self.waiting): + embed.add_field(name=f"N°{i+1}", value=ctx.guild.get_member(speaker).display_name, inline=True) + await ctx.send(embed=embed) + + @speak.group("next", pass_context=True) + async def speak_next(self, ctx: commands.Context): + if not self.voice_chan or not ctx.guild.get_channel(self.voice_chan).permissions_for(ctx.author).mute_members: + await ctx.message.add_reaction("\u274C") + else: + if self.lastSpeaker: + self.waiting.remove(self.lastSpeaker) + if self.strict: + await ctx.guild.get_member(self.lastSpeaker).edit(mute=True) + if len(self.waiting) != 0: + user : Member = ctx.guild.get_member(self.waiting[0]) + self.lastSpeaker = self.waiting[0] + await ctx.send(f"It's {user.mention} turn") + if self.strict: + await user.edit(mute=False) + else: + self.lastSpeaker = None + await ctx.send("Nobody left !") + + @speak.group("help", pass_context=True) + async def speak_help(self, ctx: commands.Context): + await ctx.send("help") + + @speak.group("setup", pass_context=True) + async def speak_setup(self, ctx: commands.Context, *args): + if not ctx.author.voice.channel.permissions_for(ctx.author).mute_members: + await ctx.message.add_reaction("\u274C") + else: + if len(args) != 0 and args[0] == "strict": + self.strict = True + for client in ctx.author.voice.channel.members: + if client != ctx.author and not client.bot: + await client.edit(mute=True) + self.voice_chan = ctx.author.voice.channel.id + await ctx.message.add_reaction("\U0001f44d") + + @speak.group("clear", pass_context=True) + async def speak_clear(self, ctx: commands.Context): + if not self.voice_chan or not ctx.guild.get_channel(self.voice_chan).permissions_for(ctx.author).mute_members: + await ctx.message.add_reaction("\u274C") + else: + self.waiting = [] + self.lastSpeaker = None + await self.speak_unmute(ctx) + self.strict = False + self.voice_chan = None + await ctx.message.add_reaction("\U0001f44d") + + @speak.group("mute", pass_context=True) + async def speak_mute(self, ctx: commands.Context): + if not self.voice_chan or not ctx.guild.get_channel(self.voice_chan).permissions_for(ctx.author).mute_members or not self.voice_chan: + await ctx.message.add_reaction("\u274C") + else: + for client in ctx.author.voice.channel.members: + if client != ctx.author and not client.bot: + await client.edit(mute=True) + await ctx.message.add_reaction("\U0001f44d") + + @speak.group("unmute", pass_context=True) + async def speak_unmute(self, ctx: commands.Context): + if not self.voice_chan or not ctx.guild.get_channel(self.voice_chan).permissions_for(ctx.author).mute_members or not self.voice_chan: + await ctx.message.add_reaction("\u274C") + else: + for client in ctx.author.voice.channel.members: + if client != ctx.author and not client.bot: + await client.edit(mute=False) + await ctx.message.add_reaction("\U0001f44d") + + @commands.Cog.listener() + async def on_voice_state_update(self, member: Member, before: VoiceState, after: VoiceState): + if (before is None or before.channel is None or before.channel.id != self.voice_chan) and\ + (after is not None and after.channel is not None and after.channel.id == self.voice_chan and self.strict): + await member.edit(mute=True) + elif (before is not None or before.channel is not None and before.channel.id == self.voice_chan) and\ + (after is None or after.channel is None or after.channel.id != self.voice_chan): + await member.edit(mute=False) + + @commands.Cog.listener() + async def on_command_error(self, ctx: commands.Context, error): + await ctx.send("An error occurred !") + raise error + + +def setup(bot): + logger.info(f"Loading...") + try: + bot.add_cog(Speak(bot)) + except Exception as e: + logger.error(f"Error loading: {e}") + else: + logger.info(f"Load successful") + + +def teardown(bot): + logger.info(f"Unloading...") + try: + bot.remove_cog("Speak") + except Exception as e: + logger.error(f"Error unloading: {e}") + else: + logger.info(f"Unload successful")