Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dpy2.0 avatar fixes #206

Open
wants to merge 14 commits into
base: dpy2.0
Choose a base branch
from
310 changes: 123 additions & 187 deletions docs/instantcommands.rst

Large diffs are not rendered by default.

135 changes: 91 additions & 44 deletions instantcmd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import logging
import importlib.util

from typing import TYPE_CHECKING, Dict
from .instantcmd import InstantCommands

from redbot.core.data_manager import cog_data_path
from redbot.core.errors import CogLoadError
from laggron_utils import init_logger

if TYPE_CHECKING:
from redbot.core.bot import Red
from redbot.core import Config

if not importlib.util.find_spec("laggron_utils"):
raise CogLoadError(
"You need the `laggron_utils` package for any cog from Laggron's Dumb Cogs. "
Expand All @@ -17,54 +23,95 @@
log = logging.getLogger("red.laggron.instantcmd")


async def ask_reset(bot, commands):
owner = bot.get_user(bot.owner_id)
async def save_old_commands(bot: "Red", config: "Config", data: Dict[str, Dict[str, str]]):
# save data
path = cog_data_path(None, raw_name="InstantCommands") / "pre-2.0-backup"
path.mkdir(exist_ok=True)

commands = data.get("commands", {})
dev_values = data.get("dev_values", {})
commands_file_path = path / "commands.py"
dev_values_file_path = path / "dev_env_values.py"

def check(message):
return message.author == owner and message.channel == owner.dm_channel
if commands:
with commands_file_path.open("w") as file:
for name, content in commands.items():
file.write("# ====================================\n")
file.write(f'# command or listener "{name}"\n')
file.write("# ====================================\n\n")
file.write(content)
file.write("\n\n\n")
log.info(f"Backed up commands and listeners at {commands_file_path.absolute()}")

if not owner:
await owner.send(
"InstantCommands was updated to its first release! Internal code was modified "
"to allow more features to be implemented, such as using `bot` in listeners "
"or storing values with Config.\n"
"It is needed to reset all of your instant commands and listeners to be "
"ready for this version.\n\n"
"**Modifications to bring:** Instead of providing only the desired function, "
"you can now put whatever you want in your code snippet, but you must return "
"your command/function at the end.\n\n"
"Example:\n"
"```py\n"
"@commands.command()\n"
"async def hello(ctx):\n"
' await ctx.send("Hello world!")\n\n'
"return hello\n"
"```"
)
path = cog_data_path(None, raw_name="InstantCommands") / "commands_backup.txt"
with path.open(mode="w") as file:
text = ""
for name, command in commands.items():
text += f"[Command/listener: {name}]\n{command}\n\n\n"
file.write(text)
log.info(f"Wrote backup file at {path}")
await owner.send(
"A file was successfully written as a backup of what you previously did at the "
f"following path:\n```{str(path)}```\n"
"Please read the docs if you want to know what exactly changed, and what you must "
"do\nhttps://laggrons-dumb-cogs.readthedocs.io/instantcommands.html"
)
if dev_values:
with dev_values_file_path.open("w") as file:
for name, content in commands.items():
file.write("# ====================================\n")
file.write(f'# dev env value "{name}"\n')
file.write("# ====================================\n\n")
file.write(content)
file.write("\n\n\n")
log.info(f"Backed up dev env values at {dev_values_file_path.absolute()}")

await config.commands.clear()
await config.dev_values.clear()
log.warning("Deleted old data")

await bot.send_to_owners(
"**InstantCommands was updated to version 2.0!**\n"
"The cog changed a lot, and even more new features are on the way. A lot of internal "
"changes were done, which means it's migration time again! Don't worry, there shouldn't "
"be much stuff to change.\n\n\n"
"**Modifications to bring:**\n\n"
"- **Commands:** Nothing is changed, but that had to be reset anyway for internal "
"reasons :D (they were mixed with listeners, now it's separated)\n\n"
"- **Listeners:** All listeners now require the decorator `instantcmd.utils.listener`. "
"Example:\n"
"```py\n"
"from instantcmd.utils import listener\n\n"
"@listener()\n"
"async def on_member_join(member):\n"
' await member.send("Welcome new member!") # don\'t do this\n\n'
"return on_member_join\n"
"```\n\n"
"- **Dev env values:** Important changes for this, they have to be added like commands "
"in the following form:\n"
"```py\n"
"from instantcmd.utils import dev_env_value\n\n"
"@dev_env_value()\n"
"def fluff_derg(ctx):\n"
" ID = 215640856839979008\n"
" if ctx.guild:\n"
" return ctx.guild.get_member(ID) or bot.get_user(ID)\n"
" else:\n"
" return bot.get_user(ID)\n\n"
"return fluff_derg\n"
"```\n\n"
"A backup of your old commands and listeners was done in "
f"`{commands_file_path.absolute()}`\n"
"A backup of your old dev_env_values was done in "
f"`{dev_values_file_path.absolute()}`\n\n"
"The old config was removed, open these files and add the commands back, you should be "
"good to go!\n"
"Now there are only two commands, `create` and `list`, the rest is done through "
"components. Anything can be toggled on/off in a click (without deletion), and more "
"supported objects are on the way, like application commands, message components and "
"cogs!\n"
"By the way, glossary change due to the increasing number of supported objects, we're not "
'referring to "commands" anymore, but "code snippets". The cog will keep its name.'
)


async def setup(bot):
async def setup(bot: "Red"):
init_logger(log, InstantCommands.__class__.__name__, "instantcmd")
n = InstantCommands(bot)
if not await n.data.updated_body():
commands = await n.data.commands()
if commands:
# the data is outdated and must be cleaned to be ready for the new version
await ask_reset(bot, commands)
await n.data.commands.set({})
await n.data.updated_body.set(True)
bot.add_cog(n)
global_data = await n.data.all()
if global_data.get("commands", {}) or global_data.get("dev_values", {}):
log.info("Detected data from previous version, starting backup and removal")
try:
await save_old_commands(bot, n.data, global_data)
except Exception:
log.critical("Failed to backup and remove data for 2.0 update!", exc_info=True)
raise CogLoadError("The cog failed to backup data for the 2.0 update!")
await bot.add_cog(n)
log.debug("Cog successfully loaded on the instance.")
77 changes: 77 additions & 0 deletions instantcmd/code_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import os
import sys
import textwrap

from typing import TypeVar, Type, Dict, Any

from redbot.core import commands

from instantcmd.core import (
CodeSnippet,
CommandSnippet,
DevEnvSnippet,
ListenerSnippet,
ExecutionException,
UnknownType,
)
from instantcmd.core.listener import Listener
from instantcmd.core.dev_env_value import DevEnv

T = TypeVar("T")
OBJECT_TYPES_MAPPING = {
commands.Command: CommandSnippet,
Listener: ListenerSnippet,
DevEnv: DevEnvSnippet,
}


# from DEV cog, made by Cog Creators (tekulvw)
def cleanup_code(content):
"""Automatically removes code blocks from the code."""
# remove ```py\n```
if content.startswith("```") and content.endswith("```"):
return "\n".join(content.split("\n")[1:-1])

# remove `foo`
return content.strip("` \n")


def get_code_from_str(content: str, env: Dict[str, Any]) -> T:
"""
Execute a string, and try to get a function from it.
"""
# The Python code is wrapped inside a function
to_compile = "def func():\n%s" % textwrap.indent(content, " ")

# Setting the instantcmd cog available in path, allowing imports like instantcmd.utils
sys.path.append(os.path.dirname(__file__))
try:
exec(to_compile, env)
except Exception as e:
raise ExecutionException("Failed to compile the code") from e
finally:
sys.path.remove(os.path.dirname(__file__))

# Execute and get the return value of the function
try:
result = env["func"]()
except Exception as e:
raise ExecutionException("Failed to execute the function") from e

# Function does not have a return value
if not result:
raise ExecutionException("Nothing detected. Make sure to return something")
return result


def find_matching_type(code: T) -> Type[CodeSnippet]:
for source, dest in OBJECT_TYPES_MAPPING.items():
if isinstance(code, source):
return dest
if hasattr(code, "__name__"):
raise UnknownType(
f"The function `{code.__name__}` needs to be transformed into something. "
"Did you forget a decorator?"
)
else:
raise UnknownType(f"The type `{type(code)}` is currently not supported by instantcmd.")
Loading