mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
First commit
This commit is contained in:
parent
6251c585e4
commit
2063decbe7
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.json
|
||||
*.pyc
|
||||
__pycache__
|
||||
*.exe
|
||||
*.dll
|
||||
*.log
|
||||
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Red - Discord Bot v3
|
||||
|
||||
**This is alpha. Regular use is not recommended.
|
||||
There will not be any effort not to break current installations.**
|
||||
5
cogs/audio/__init__.py
Normal file
5
cogs/audio/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .audio import Audio
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Audio(bot))
|
||||
112
cogs/audio/audio.py
Normal file
112
cogs/audio/audio.py
Normal file
@ -0,0 +1,112 @@
|
||||
from discord.ext import commands
|
||||
from discord import FFmpegPCMAudio, PCMVolumeTransformer
|
||||
import os
|
||||
import youtube_dl
|
||||
import discord
|
||||
|
||||
|
||||
# Just a little experimental audio cog not meant for final release
|
||||
|
||||
|
||||
class Audio:
|
||||
"""Audio commands"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command()
|
||||
async def local(self, ctx, *, filename: str):
|
||||
"""Play mp3"""
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send("Join a voice channel first!")
|
||||
return
|
||||
|
||||
if ctx.guild.voice_client:
|
||||
if ctx.guild.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.guild.voice_client.disconnect()
|
||||
path = os.path.join("cogs", "audio", "songs", filename + ".mp3")
|
||||
if not os.path.isfile(path):
|
||||
await ctx.send("Let's play a file that exists pls")
|
||||
return
|
||||
player = PCMVolumeTransformer(FFmpegPCMAudio(path), volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send("{} is playing a song...".format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def play(self, ctx, url: str):
|
||||
"""Play youtube url"""
|
||||
url = url.strip("<").strip(">")
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send("Join a voice channel first!")
|
||||
return
|
||||
elif "youtube.com" not in url.lower():
|
||||
await ctx.send("Youtube links pls")
|
||||
return
|
||||
|
||||
if ctx.guild.voice_client:
|
||||
if ctx.guild.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.guild.voice_client.disconnect()
|
||||
yt = YoutubeSource(url)
|
||||
player = PCMVolumeTransformer(yt, volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send("{} is playing a song...".format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def stop(self, ctx):
|
||||
"""Stops the music and disconnects"""
|
||||
if ctx.guild.voice_client:
|
||||
ctx.guild.voice_client.source.cleanup()
|
||||
await ctx.guild.voice_client.disconnect()
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command()
|
||||
async def pause(self, ctx):
|
||||
"""Pauses the music"""
|
||||
if ctx.guild.voice_client:
|
||||
ctx.guild.voice_client.pause()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command()
|
||||
async def resume(self, ctx):
|
||||
"""Resumes the music"""
|
||||
if ctx.guild.voice_client:
|
||||
ctx.guild.voice_client.resume()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def volume(self, ctx, n: float):
|
||||
"""Sets the volume"""
|
||||
if ctx.guild.voice_client:
|
||||
ctx.guild.voice_client.source.volume = n
|
||||
await ctx.send("Volume set.", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
def __unload(self):
|
||||
for vc in self.bot.voice_clients:
|
||||
if vc.source:
|
||||
vc.source.cleanup()
|
||||
self.bot.loop.create_task(vc.disconnect())
|
||||
|
||||
|
||||
class YoutubeSource(discord.FFmpegPCMAudio):
|
||||
def __init__(self, url):
|
||||
opts = {
|
||||
'format': 'webm[abr>0]/bestaudio/best',
|
||||
'prefer_ffmpeg': True,
|
||||
'quiet': True
|
||||
}
|
||||
ytdl = youtube_dl.YoutubeDL(opts)
|
||||
self.info = ytdl.extract_info(url, download=False)
|
||||
super().__init__(self.info['url'])
|
||||
4
core/__init__.py
Normal file
4
core/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .owner import Owner
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Owner(bot))
|
||||
61
core/bot.py
Normal file
61
core/bot.py
Normal file
@ -0,0 +1,61 @@
|
||||
from discord.ext import commands
|
||||
from collections import Counter
|
||||
from core.utils.helpers import JsonGuildDB
|
||||
import os
|
||||
|
||||
|
||||
class Red(commands.Bot):
|
||||
def __init__(self, cli_flags, **kwargs):
|
||||
self._shutdown_mode = None
|
||||
self.db = JsonGuildDB("core/data/settings.json",
|
||||
autosave=True,
|
||||
create_dirs=True)
|
||||
|
||||
def prefix_manager(bot, message):
|
||||
global_prefix = self.db.get_global("prefix", [])
|
||||
if message.guild is None:
|
||||
return global_prefix
|
||||
server_prefix = self.db.get(message.guild, "prefix", [])
|
||||
return server_prefix if server_prefix else global_prefix
|
||||
|
||||
# Priority: args passed > cli flags > db
|
||||
if "command_prefix" not in kwargs:
|
||||
if cli_flags.prefix:
|
||||
kwargs["command_prefix"] = lambda bot, message: cli_flags.prefix
|
||||
else:
|
||||
kwargs["command_prefix"] = None
|
||||
|
||||
if kwargs["command_prefix"] is None:
|
||||
kwargs["command_prefix"] = prefix_manager
|
||||
|
||||
self.counter = Counter()
|
||||
self.uptime = None
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def is_owner(self, user, allow_coowners=True):
|
||||
if allow_coowners:
|
||||
if user.id in self.settings.coowners:
|
||||
return True
|
||||
return await super().is_owner(user)
|
||||
|
||||
async def send_cmd_help(self, ctx):
|
||||
if ctx.invoked_subcommand:
|
||||
pages = await self.formatter.format_help_for(ctx, ctx.invoked_subcommand)
|
||||
for page in pages:
|
||||
await ctx.send(page)
|
||||
else:
|
||||
pages = await self.formatter.format_help_for(ctx, ctx.command)
|
||||
for page in pages:
|
||||
await ctx.send(page)
|
||||
|
||||
async def logout(self, *, restart=False):
|
||||
"""Gracefully quits Red with exit code 0
|
||||
|
||||
If restart is True, the exit code will be 26 instead
|
||||
Upon receiving that exit code, the launcher restarts Red"""
|
||||
self._shutdown_mode = not restart
|
||||
await super().logout()
|
||||
|
||||
def list_packages(self):
|
||||
"""Lists packages present in the cogs the folder"""
|
||||
return os.listdir("cogs")
|
||||
51
core/db.py
Normal file
51
core/db.py
Normal file
@ -0,0 +1,51 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
DEFAULT_JSON_SETTINGS = {
|
||||
"indent": 4,
|
||||
"sort_keys": True,
|
||||
"separators": (',', ' : ')
|
||||
}
|
||||
|
||||
|
||||
class JSONAutosave:
|
||||
def __init__(self, interval=5, **settings):
|
||||
self.interval = interval
|
||||
self._queue = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._json_settings = settings.pop("json_settings",
|
||||
DEFAULT_JSON_SETTINGS)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.task = self.loop.create_task(self._process_queue())
|
||||
|
||||
def add_to_queue(self, path, data):
|
||||
self._queue[path] = data
|
||||
|
||||
async def _process_queue(self):
|
||||
try:
|
||||
while True:
|
||||
print("looping")
|
||||
queue = self._queue.copy()
|
||||
self._queue = {}
|
||||
for path, data in queue.items():
|
||||
with await self._lock:
|
||||
func = functools.partial(self._save_json, path, data)
|
||||
try:
|
||||
await self.loop.run_in_executor(None, func)
|
||||
except Exception as e:
|
||||
print(e) # Proper logging here
|
||||
|
||||
await asyncio.sleep(self.interval)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def _save_json(self, path, data):
|
||||
print("Saving " + path)
|
||||
path, _ = os.path.splitext(path)
|
||||
tmp_file = "{}-{}.tmp".format(path, uuid4().fields[0])
|
||||
with open(tmp_file, encoding="utf-8", mode="w") as f:
|
||||
json.dump(data, f, **self._json_settings)
|
||||
os.replace(tmp_file, path)
|
||||
85
core/events.py
Normal file
85
core/events.py
Normal file
@ -0,0 +1,85 @@
|
||||
import discord
|
||||
import traceback
|
||||
import datetime
|
||||
import logging
|
||||
from discord.ext import commands
|
||||
from core.utils.chat_formatting import inline
|
||||
|
||||
log = logging.getLogger("red")
|
||||
|
||||
|
||||
def init_events(bot):
|
||||
|
||||
@bot.event
|
||||
async def on_connect():
|
||||
if bot.uptime is None:
|
||||
print("Connected to Discord. Getting ready...")
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
if bot.uptime is None:
|
||||
bot.uptime = datetime.datetime.utcnow()
|
||||
print("Loading cogs...")
|
||||
# load the packages at this point
|
||||
total_channels = len([c for c in bot.get_all_channels()])
|
||||
total_users = len(set([m for m in bot.get_all_members()]))
|
||||
print("Ready and operational on {} servers.\n"
|
||||
"".format(len(bot.guilds)))
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(error, ctx):
|
||||
if isinstance(error, commands.MissingRequiredArgument):
|
||||
await bot.send_cmd_help(ctx)
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
await bot.send_cmd_help(ctx)
|
||||
elif isinstance(error, commands.DisabledCommand):
|
||||
await ctx.send("That command is disabled.")
|
||||
elif isinstance(error, commands.CommandInvokeError):
|
||||
# Need to test if the following still works
|
||||
"""
|
||||
no_dms = "Cannot send messages to this user"
|
||||
is_help_cmd = ctx.command.qualified_name == "help"
|
||||
is_forbidden = isinstance(error.original, discord.Forbidden)
|
||||
if is_help_cmd and is_forbidden and error.original.text == no_dms:
|
||||
msg = ("I couldn't send the help message to you in DM. Either"
|
||||
" you blocked me or you disabled DMs in this server.")
|
||||
await ctx.send(msg)
|
||||
return
|
||||
"""
|
||||
log.exception("Exception in command '{}'"
|
||||
"".format(ctx.command.qualified_name),
|
||||
exc_info=error.original)
|
||||
message = ("Error in command '{}'. Check your console or "
|
||||
"logs for details."
|
||||
"".format(ctx.command.qualified_name))
|
||||
exception_log = ("Exception in command '{}'\n"
|
||||
"".format(ctx.command.qualified_name))
|
||||
exception_log += "".join(traceback.format_exception(type(error),
|
||||
error, error.__traceback__))
|
||||
bot._last_exception = exception_log
|
||||
await ctx.send(inline(message))
|
||||
elif isinstance(error, commands.CommandNotFound):
|
||||
pass
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
await ctx.send("⛔")
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
await ctx.send("That command is not available in DMs.")
|
||||
elif isinstance(error, commands.CommandOnCooldown):
|
||||
await ctx.send("This command is on cooldown. "
|
||||
"Try again in {:.2f}s"
|
||||
"".format(error.retry_after))
|
||||
else:
|
||||
log.exception(type(error).__name__, exc_info=error)
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
bot.counter["messages_read"] += 1
|
||||
await bot.process_commands(message)
|
||||
|
||||
@bot.event
|
||||
async def on_resumed():
|
||||
bot.counter["sessions_resumed"] += 1
|
||||
|
||||
@bot.event
|
||||
async def on_command(command):
|
||||
bot.counter["processed_commands"] += 1
|
||||
30
core/global_checks.py
Normal file
30
core/global_checks.py
Normal file
@ -0,0 +1,30 @@
|
||||
def init_global_checks(bot):
|
||||
|
||||
@bot.check
|
||||
async def global_perms(ctx):
|
||||
if await bot.is_owner(ctx.author):
|
||||
return True
|
||||
|
||||
if bot.db.get_global("whitelist", []):
|
||||
return ctx.author.id in bot.db.get_global("whitelist", [])
|
||||
|
||||
return ctx.author.id not in bot.db.get_global("blacklist", [])
|
||||
|
||||
@bot.check
|
||||
async def local_perms(ctx):
|
||||
if await bot.is_owner(ctx.author):
|
||||
return True
|
||||
elif ctx.message.guild is None:
|
||||
return True
|
||||
guild_perms = bot.db.get_all(ctx.guild, {})
|
||||
local_blacklist = guild_perms.get("blacklist", [])
|
||||
local_whitelist = guild_perms.get("whitelist", [])
|
||||
|
||||
if local_whitelist:
|
||||
return ctx.author.id in local_whitelist
|
||||
|
||||
return ctx.author.id not in local_blacklist
|
||||
|
||||
@bot.check
|
||||
async def bots(ctx):
|
||||
return not ctx.author.bot
|
||||
0
core/interactive_config.py
Normal file
0
core/interactive_config.py
Normal file
75
core/json_flusher.py
Normal file
75
core/json_flusher.py
Normal file
@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from core.json_io import JsonIO, PRETTY
|
||||
|
||||
# This is where individual cogs can queue low priority writes to files
|
||||
#
|
||||
# Only the last queued write to a file actually gets executed.
|
||||
# This helps considerably in reducing the total writes (especially in poorly
|
||||
# coded cogs that would otherwise hammer the system with them)
|
||||
#
|
||||
# The flusher is used by the DB helpers in autosave mode
|
||||
#
|
||||
# The JSONFlusher class is supposed to be instanced only once, at boot
|
||||
|
||||
log = logging.getLogger("red")
|
||||
_flusher = None
|
||||
|
||||
|
||||
class JSONFlusher(JsonIO):
|
||||
def __init__(self, interval=5, **settings):
|
||||
self.interval = interval
|
||||
self._queue = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._json_settings = settings.pop("json_settings", PRETTY)
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self.task = self._loop.create_task(self._process_queue())
|
||||
|
||||
def add_to_queue(self, path, data):
|
||||
"""Schedules a json file for later write
|
||||
|
||||
Calling this function multiple times with the same path will
|
||||
result in only the last one getting scheduled"""
|
||||
self._queue[path] = data
|
||||
|
||||
def remove_from_queue(self, path):
|
||||
"""Removes json file from the writing queue"""
|
||||
try:
|
||||
del self._queue[path]
|
||||
except:
|
||||
pass
|
||||
|
||||
async def _process_queue(self):
|
||||
try:
|
||||
while True:
|
||||
queue = self._queue.copy()
|
||||
self._queue = {}
|
||||
for path, data in queue.items():
|
||||
with await self._lock:
|
||||
try:
|
||||
await self._threadsafe_save_json(path,
|
||||
data,
|
||||
self._json_settings)
|
||||
except Exception as e:
|
||||
log.critical("Flusher failed to write: {}"
|
||||
"".format(e))
|
||||
await asyncio.sleep(self.interval)
|
||||
except asyncio.CancelledError:
|
||||
if self._queue:
|
||||
log.warning("Flusher interrupted with "
|
||||
"non-empty queue")
|
||||
else:
|
||||
log.debug("Flusher shutting down.")
|
||||
|
||||
|
||||
def init_flusher():
|
||||
"""Instances the flusher and initializes its task"""
|
||||
global _flusher
|
||||
_flusher = JSONFlusher()
|
||||
|
||||
|
||||
def get_flusher():
|
||||
"""Returns the global flusher instance"""
|
||||
if _flusher is None:
|
||||
raise RuntimeError("The flusher has not been initialized.")
|
||||
return _flusher
|
||||
46
core/json_io.py
Normal file
46
core/json_io.py
Normal file
@ -0,0 +1,46 @@
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
# This is basically our old DataIO, except that it's now threadsafe
|
||||
# and just a base for much more elaborate classes
|
||||
|
||||
|
||||
log = logging.getLogger("red")
|
||||
|
||||
PRETTY = {"indent": 4, "sort_keys": True, "separators": (',', ' : ')}
|
||||
MINIFIED = {"sort_keys": True, "separators": (',', ':')}
|
||||
|
||||
|
||||
class JsonIO:
|
||||
"""Basic functions for atomic saving / loading of json files
|
||||
|
||||
This is inherited by the flusher and db helpers"""
|
||||
|
||||
def _save_json(self, path, data, settings=PRETTY):
|
||||
log.debug("Saving file {}".format(path))
|
||||
filename, _ = os.path.splitext(path)
|
||||
tmp_file = "{}-{}.tmp".format(filename, uuid4().fields[0])
|
||||
with open(tmp_file, encoding="utf-8", mode="w") as f:
|
||||
json.dump(data, f, **settings)
|
||||
os.replace(tmp_file, path)
|
||||
|
||||
async def _threadsafe_save_json(self, path, data, settings=PRETTY):
|
||||
loop = asyncio.get_event_loop()
|
||||
func = functools.partial(self._save_json, path, data, settings)
|
||||
await loop.run_in_executor(None, func)
|
||||
|
||||
def _load_json(self, path):
|
||||
log.debug("Reading file {}".format(path))
|
||||
with open(path, encoding='utf-8', mode="r") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
async def _threadsafe_load_json(self, path):
|
||||
loop = asyncio.get_event_loop()
|
||||
func = functools.partial(self._load_json, path)
|
||||
task = loop.run_in_executor(None, func)
|
||||
return await asyncio.wait_for(task)
|
||||
115
core/owner.py
Normal file
115
core/owner.py
Normal file
@ -0,0 +1,115 @@
|
||||
from discord.ext import commands
|
||||
from core.utils import checks
|
||||
from core.utils.chat_formatting import box
|
||||
import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import discord
|
||||
|
||||
|
||||
class Owner:
|
||||
"""All owner-only commands that relate to debug bot operations."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def load(self, ctx, *, cog_name: str):
|
||||
"""Loads a package"""
|
||||
if not cog_name.startswith("cogs."):
|
||||
cog_name = "cogs." + cog_name
|
||||
self.bot.load_extension(cog_name)
|
||||
await ctx.send("Done.")
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def unload(self, ctx, *, cog_name: str):
|
||||
"""Unloads a package"""
|
||||
if not cog_name.startswith("cogs."):
|
||||
cog_name = "cogs." + cog_name
|
||||
if cog_name in self.bot.extensions:
|
||||
self.bot.unload_extension(cog_name)
|
||||
await ctx.send("Done.")
|
||||
else:
|
||||
await ctx.send("That extension is not loaded.")
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
async def _reload(self, ctx, *, cog_name: str):
|
||||
"""Reloads a package"""
|
||||
if not cog_name.startswith("cogs."):
|
||||
cog_name = "cogs." + cog_name
|
||||
self.refresh_modules(cog_name)
|
||||
self.bot.unload_extension(cog_name)
|
||||
self.bot.load_extension(cog_name)
|
||||
await ctx.send("Done.")
|
||||
|
||||
def refresh_modules(self, module):
|
||||
"""Interally reloads modules so that changes are detected"""
|
||||
module = module.replace(".", os.sep)
|
||||
for root, dirs, files in os.walk(module):
|
||||
for name in files:
|
||||
if name.endswith(".py"):
|
||||
path = os.path.join(root, name)
|
||||
path, _ = os.path.splitext(path)
|
||||
path = ".".join(path.split(os.sep))
|
||||
print("Reloading " + path)
|
||||
m = importlib.import_module(path)
|
||||
importlib.reload(m)
|
||||
|
||||
@commands.command(hidden=True)
|
||||
@checks.is_owner()
|
||||
async def debug(self, ctx, *, code):
|
||||
"""Evaluates code"""
|
||||
author = ctx.message.author
|
||||
channel = ctx.message.channel
|
||||
|
||||
code = code.strip('` ')
|
||||
result = None
|
||||
|
||||
global_vars = globals().copy()
|
||||
global_vars['bot'] = self.bot
|
||||
global_vars['ctx'] = ctx
|
||||
global_vars['message'] = ctx.message
|
||||
global_vars['author'] = ctx.message.author
|
||||
global_vars['channel'] = ctx.message.channel
|
||||
global_vars['guild'] = ctx.message.guild
|
||||
|
||||
try:
|
||||
result = eval(code, global_vars, locals())
|
||||
except Exception as e:
|
||||
await ctx.send('```py\n{}: {}```'.format(type(e).__name__, str(e)),)
|
||||
return
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
result = str(result)
|
||||
|
||||
if ctx.message.guild is not None:
|
||||
token = ctx.bot.http.token
|
||||
r = "[EXPUNGED]"
|
||||
result = result.replace(token, r)
|
||||
result = result.replace(token.lower(), r)
|
||||
result = result.replace(token.upper(), r)
|
||||
|
||||
await ctx.send(box(result, lang="py"))
|
||||
|
||||
@commands.command(hidden=True)
|
||||
@checks.is_owner()
|
||||
async def mock(self, ctx, user: discord.Member, *, command):
|
||||
"""Runs a command as if it was issued by a different user
|
||||
|
||||
The prefix must not be entered"""
|
||||
# Since we have stateful objects now this might be pretty bad
|
||||
# Sorry Danny
|
||||
old_author = ctx.author
|
||||
old_content = ctx.message.content
|
||||
ctx.message.author = user
|
||||
ctx.message.content = ctx.prefix + command
|
||||
|
||||
await self.bot.process_commands(ctx.message)
|
||||
|
||||
ctx.message.author = old_author
|
||||
ctx.message.content = old_content
|
||||
57
core/settings.py
Normal file
57
core/settings.py
Normal file
@ -0,0 +1,57 @@
|
||||
import argparse
|
||||
|
||||
# Do we even need a Settings class this time? To be decided
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self):
|
||||
args = {}
|
||||
self.coowners = []
|
||||
|
||||
def can_login(self):
|
||||
"""Used on start to determine if Red is setup enough to login"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def parse_cli_flags():
|
||||
parser = argparse.ArgumentParser(description="Red - Discord Bot")
|
||||
parser.add_argument("--owner", help="ID of the owner. Only who hosts "
|
||||
"Red should be owner, this has "
|
||||
"security implications")
|
||||
parser.add_argument("--prefix", "-p", action="append",
|
||||
help="Global prefix. Can be multiple")
|
||||
parser.add_argument("--no-prompt",
|
||||
action="store_true",
|
||||
help="Disables console inputs. Features requiring "
|
||||
"console interaction could be disabled as a "
|
||||
"result")
|
||||
parser.add_argument("--no-cogs",
|
||||
action="store_true",
|
||||
help="Starts Red with no cogs loaded, only core")
|
||||
parser.add_argument("--self-bot",
|
||||
action='store_true',
|
||||
help="Specifies if Red should log in as selfbot")
|
||||
parser.add_argument("--not-bot",
|
||||
action='store_true',
|
||||
help="Specifies if the token used belongs to a bot "
|
||||
"account.")
|
||||
parser.add_argument("--dry-run",
|
||||
action="store_true",
|
||||
help="Makes Red quit with code 0 just before the "
|
||||
"login. This is useful for testing the boot "
|
||||
"process.")
|
||||
parser.add_argument("--debug",
|
||||
action="store_true",
|
||||
help="Sets the loggers level as debug")
|
||||
parser.add_argument("--dev",
|
||||
action="store_true",
|
||||
help="Enables developer mode")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.prefix:
|
||||
args.prefix = sorted(args.prefix, reverse=True)
|
||||
else:
|
||||
args.prefix = []
|
||||
|
||||
return args
|
||||
0
core/utils/__init__.py
Normal file
0
core/utils/__init__.py
Normal file
76
core/utils/chat_formatting.py
Normal file
76
core/utils/chat_formatting.py
Normal file
@ -0,0 +1,76 @@
|
||||
def error(text):
|
||||
return "\N{NO ENTRY SIGN} {}".format(text)
|
||||
|
||||
|
||||
def warning(text):
|
||||
return "\N{WARNING SIGN} {}".format(text)
|
||||
|
||||
|
||||
def info(text):
|
||||
return "\N{INFORMATION SOURCE} {}".format(text)
|
||||
|
||||
|
||||
def question(text):
|
||||
return "\N{BLACK QUESTION MARK ORNAMENT} {}".format(text)
|
||||
|
||||
|
||||
def bold(text):
|
||||
return "**{}**".format(text)
|
||||
|
||||
|
||||
def box(text, lang=""):
|
||||
ret = "```{}\n{}\n```".format(lang, text)
|
||||
return ret
|
||||
|
||||
|
||||
def inline(text):
|
||||
return "`{}`".format(text)
|
||||
|
||||
|
||||
def italics(text):
|
||||
return "*{}*".format(text)
|
||||
|
||||
|
||||
def pagify(text, delims=["\n"], *, escape=True, shorten_by=8,
|
||||
page_length=2000):
|
||||
"""DOES NOT RESPECT MARKDOWN BOXES OR INLINE CODE"""
|
||||
in_text = text
|
||||
if escape:
|
||||
num_mentions = text.count("@here") + text.count("@everyone")
|
||||
shorten_by += num_mentions
|
||||
page_length -= shorten_by
|
||||
while len(in_text) > page_length:
|
||||
closest_delim = max([in_text.rfind(d, 0, page_length)
|
||||
for d in delims])
|
||||
closest_delim = closest_delim if closest_delim != -1 else page_length
|
||||
if escape:
|
||||
to_send = escape_mass_mentions(in_text[:closest_delim])
|
||||
else:
|
||||
to_send = in_text[:closest_delim]
|
||||
yield to_send
|
||||
in_text = in_text[closest_delim:]
|
||||
|
||||
if escape:
|
||||
yield escape_mass_mentions(in_text)
|
||||
else:
|
||||
yield in_text
|
||||
|
||||
|
||||
def strikethrough(text):
|
||||
return "~~{}~~".format(text)
|
||||
|
||||
|
||||
def underline(text):
|
||||
return "__{}__".format(text)
|
||||
|
||||
|
||||
def escape(text, *, mass_mentions=False, formatting=False):
|
||||
if mass_mentions:
|
||||
text = text.replace("@everyone", "@\u200beveryone")
|
||||
text = text.replace("@here", "@\u200bhere")
|
||||
if formatting:
|
||||
text = (text.replace("`", "\\`")
|
||||
.replace("*", "\\*")
|
||||
.replace("_", "\\_")
|
||||
.replace("~", "\\~"))
|
||||
return text
|
||||
7
core/utils/checks.py
Normal file
7
core/utils/checks.py
Normal file
@ -0,0 +1,7 @@
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
def is_owner(**kwargs):
|
||||
async def check(ctx):
|
||||
return await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||
return commands.check(check)
|
||||
200
core/utils/helpers.py
Normal file
200
core/utils/helpers.py
Normal file
@ -0,0 +1,200 @@
|
||||
import os
|
||||
import discord
|
||||
from collections import defaultdict
|
||||
from core.json_io import JsonIO
|
||||
from core import json_flusher
|
||||
|
||||
|
||||
GLOBAL_KEY = '__global__'
|
||||
SENTINEL = object()
|
||||
|
||||
|
||||
class JsonDB(JsonIO):
|
||||
"""
|
||||
A DB-like helper class to streamline the saving of json files
|
||||
|
||||
Parameters:
|
||||
|
||||
file_path: str
|
||||
The path of the json file you want to create / access
|
||||
create_dirs: bool=False
|
||||
If True, it will create any missing directory leading to
|
||||
the file you want to create
|
||||
autosave: bool=False
|
||||
If True, any change to the "database" will be queued to the
|
||||
flusher and scheduled for a later write
|
||||
default_value: Optional=None
|
||||
Same behaviour as a defaultdict
|
||||
"""
|
||||
def __init__(self, file_path, **kwargs):
|
||||
create_dirs = kwargs.pop("create_dirs", False)
|
||||
default_value = kwargs.pop("default_value", SENTINEL)
|
||||
self.autosave = kwargs.pop("autosave", False)
|
||||
self.path = file_path
|
||||
self._flusher = json_flusher.get_flusher()
|
||||
|
||||
file_exists = os.path.isfile(file_path)
|
||||
|
||||
if create_dirs and not file_exists:
|
||||
path, _ = os.path.split(file_path)
|
||||
if path:
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
if file_exists:
|
||||
# Might be worth looking into threadsafe ways for very large files
|
||||
self._data = self._load_json(file_path)
|
||||
else:
|
||||
self._data = {}
|
||||
self._save()
|
||||
|
||||
if default_value is not SENTINEL:
|
||||
def _get_default():
|
||||
return default_value
|
||||
self._data = defaultdict(_get_default, self._data)
|
||||
|
||||
def set(self, key, value):
|
||||
"""Sets a DB's entry"""
|
||||
self._data[key] = value
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Returns a DB's entry"""
|
||||
return self._data.get(key, default)
|
||||
|
||||
def remove(self, key):
|
||||
"""Removes a DB's entry"""
|
||||
del self._data[key]
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
"""Removes and returns a DB's entry"""
|
||||
return self._data.pop(key, default)
|
||||
|
||||
def wipe(self):
|
||||
"""Wipes DB"""
|
||||
self._data = {}
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def all(self):
|
||||
"""Returns all DB's data"""
|
||||
return self._data
|
||||
|
||||
def _save(self):
|
||||
"""Using this should be avoided. Let's stick to threadsafe saves"""
|
||||
self._save_json(self.path, self._data)
|
||||
|
||||
async def save(self):
|
||||
self._flusher.remove_from_queue(self.path)
|
||||
await self._threadsafe_save_json(self.path, self._data)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} {}>".format(self.__class__.__name__, self._data)
|
||||
|
||||
|
||||
class JsonGuildDB(JsonDB):
|
||||
"""
|
||||
A DB-like helper class to streamline the saving of json files
|
||||
This is a variant of JsonDB that allows for guild specific data
|
||||
Global data is still allowed with dedicated methods
|
||||
|
||||
Same parameters as JsonDB
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set(self, guild, key, value):
|
||||
"""Sets a guild's entry"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only set guild data')
|
||||
if str(guild.id) not in self._data:
|
||||
self._data[str(guild.id)] = {}
|
||||
self._data[str(guild.id)][key] = value
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def get(self, guild, key, default=None):
|
||||
"""Returns a guild's entry"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only get guild data')
|
||||
if str(guild.id) not in self._data:
|
||||
return default
|
||||
return self._data[str(guild.id)].get(key, default)
|
||||
|
||||
def remove(self, guild, key):
|
||||
"""Removes a guild's entry"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only remove guild data')
|
||||
if str(guild.id) not in self._data:
|
||||
raise KeyError('Guild data is not present')
|
||||
del self._data[str(guild.id)][key]
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def pop(self, guild, key, default=None):
|
||||
"""Removes and returns a guild's entry"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only remove guild data')
|
||||
return self._data.get(str(guild.id), {}).pop(key, default)
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def get_all(self, guild, default):
|
||||
"""Returns all entries of a guild"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only get guild data')
|
||||
return self._data.get(str(guild.id), default)
|
||||
|
||||
def remove_all(self, guild):
|
||||
"""Removes all entries of a guild"""
|
||||
if not isinstance(guild, discord.Guild):
|
||||
raise TypeError('Can only remove guilds')
|
||||
super().remove(str(guild.id))
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def set_global(self, key, value):
|
||||
"""Sets a global value"""
|
||||
if GLOBAL_KEY not in self._data:
|
||||
self._data[GLOBAL_KEY] = {}
|
||||
self._data[GLOBAL_KEY][key] = value
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def get_global(self, key, default=None):
|
||||
"""Gets a global value"""
|
||||
if GLOBAL_KEY not in self._data:
|
||||
self._data[GLOBAL_KEY] = {}
|
||||
|
||||
return self._data[GLOBAL_KEY].get(key, default)
|
||||
|
||||
def remove_global(self, key):
|
||||
"""Removes a global value"""
|
||||
if GLOBAL_KEY not in self._data:
|
||||
self._data[GLOBAL_KEY] = {}
|
||||
del self._data[key]
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
|
||||
def pop_global(self, key, default=None):
|
||||
"""Removes and returns a global value"""
|
||||
if GLOBAL_KEY not in self._data:
|
||||
self._data[GLOBAL_KEY] = {}
|
||||
return self._data.pop(key, default)
|
||||
if self.autosave:
|
||||
self._flusher.add_to_queue(self.path, self._data)
|
||||
0
launcher.py
Normal file
0
launcher.py
Normal file
60
main.py
Normal file
60
main.py
Normal file
@ -0,0 +1,60 @@
|
||||
from core.bot import Red
|
||||
from core.global_checks import init_global_checks
|
||||
from core.events import init_events
|
||||
from core.json_flusher import init_flusher
|
||||
from core.settings import parse_cli_flags
|
||||
import logging.handlers
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
#
|
||||
# Red - Discord Bot v3
|
||||
#
|
||||
# Made by Twentysix, improved by many
|
||||
#
|
||||
|
||||
|
||||
def init_loggers(cli_flags):
|
||||
dpy_logger = logging.getLogger("discord")
|
||||
dpy_logger.setLevel(logging.WARNING)
|
||||
console = logging.StreamHandler()
|
||||
console.setLevel(logging.WARNING)
|
||||
dpy_logger.addHandler(console)
|
||||
|
||||
logger = logging.getLogger("red")
|
||||
|
||||
red_format = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: '
|
||||
'%(message)s',
|
||||
datefmt="[%d/%m/%Y %H:%M]")
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
stdout_handler.setFormatter(red_format)
|
||||
|
||||
if cli_flags.debug:
|
||||
os.environ['PYTHONASYNCIODEBUG'] = '1'
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
fhandler = logging.handlers.RotatingFileHandler(
|
||||
filename='red.log', encoding='utf-8', mode='a',
|
||||
maxBytes=10**7, backupCount=5)
|
||||
fhandler.setFormatter(red_format)
|
||||
|
||||
logger.addHandler(fhandler)
|
||||
logger.addHandler(stdout_handler)
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli_flags = parse_cli_flags()
|
||||
init_loggers(cli_flags)
|
||||
init_flusher()
|
||||
description = "Red v3 - Alpha"
|
||||
red = Red(cli_flags, description=description, pm_help=None)
|
||||
init_global_checks(red)
|
||||
init_events(red)
|
||||
red.load_extension('core')
|
||||
if cli_flags.dev:
|
||||
pass # load dev cog here?
|
||||
red.run(os.environ['RED_TOKEN'], bot=not cli_flags.not_bot)
|
||||
Loading…
x
Reference in New Issue
Block a user