Skip to content
Permalink
Browse files

[Core] Add `redbot --edit` cli flag (replacement for `[p]set owner&to…

…ken`) (#3060)

* feat(core): add `redbot --edit` cli flag

* chore(changelog): add towncrier entries

* refactor(core): clean up `redbot --edit`, few fixes

* fix(core): prepare for review

* chore(changelog): update towncrier entry to use double ticks :p

* style(black): ugh, Sinbad's git hook isn't perfect (using worktrees)

* fix: Address Flame's first review
  • Loading branch information...
jack1142 authored and mikeshardmind committed Nov 8, 2019
1 parent 078210b commit 1651de13051ee838325262e7ce6cafa808644bc0
Showing with 252 additions and 80 deletions.
  1. +1 −0 changelog.d/3060.enhance.rst
  2. +1 −0 changelog.d/3060.feature.rst
  3. +1 −0 changelog.d/3060.fix.rst
  4. +167 −6 redbot/__main__.py
  5. +63 −8 redbot/core/cli.py
  6. +1 −1 redbot/launcher.py
  7. +18 −65 redbot/setup.py
@@ -0,0 +1 @@
All ``y/n`` confirmations in cli commands are now unified.
@@ -0,0 +1 @@
Added ``redbot --edit`` cli flag that can be used to edit instance name, token, owner and datapath.
@@ -0,0 +1 @@
Arguments ``--co-owner`` and ``--load-cogs`` now properly require at least one argument to be passed.
@@ -6,7 +6,10 @@
import json
import logging
import os
import shutil
import sys
from copy import deepcopy
from pathlib import Path

import discord

@@ -23,6 +26,7 @@
from redbot.core.global_checks import init_global_checks
from redbot.core.events import init_events
from redbot.core.cli import interactive_config, confirm, parse_cli_flags
from redbot.setup import get_data_dir, get_name, save_config
from redbot.core.core_commands import Core
from redbot.core.dev_commands import Dev
from redbot.core import __version__, modlog, bank, data_manager, drivers
@@ -48,6 +52,12 @@
indict["prefix"] = await red._config.prefix()


def _get_instance_names():
with data_manager.config_file.open(encoding="utf-8") as fs:
data = json.load(fs)
return sorted(data.keys())


def list_instances():
if not data_manager.config_file.exists():
print(
@@ -56,15 +66,157 @@ def list_instances():
)
sys.exit(1)
else:
with data_manager.config_file.open(encoding="utf-8") as fs:
data = json.load(fs)
text = "Configured Instances:\n\n"
for instance_name in sorted(data.keys()):
for instance_name in _get_instance_names():
text += "{}\n".format(instance_name)
print(text)
sys.exit(0)


def edit_instance(red, cli_flags):
no_prompt = cli_flags.no_prompt
token = cli_flags.token
owner = cli_flags.owner
old_name = cli_flags.instance_name
new_name = cli_flags.edit_instance_name
data_path = cli_flags.edit_data_path
copy_data = cli_flags.copy_data
confirm_overwrite = cli_flags.overwrite_existing_instance

if data_path is None and copy_data:
print("--copy-data can't be used without --edit-data-path argument")
sys.exit(1)
if new_name is None and confirm_overwrite:
print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
sys.exit(1)
if no_prompt and all(to_change is None for to_change in (token, owner, new_name, data_path)):
print(
"No arguments to edit were provided. Available arguments (check help for more "
"information): --edit-instance-name, --edit-data-path, --copy-data, --owner, --token"
)
sys.exit(1)

_edit_token(red, token, no_prompt)
_edit_owner(red, owner, no_prompt)

data = deepcopy(data_manager.basic_config)
name = _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt)
_edit_data_path(data, data_path, copy_data, no_prompt)

save_config(name, data)
if old_name != name:
save_config(old_name, {}, remove=True)


def _edit_token(red, token, no_prompt):
if token:
if not len(token) >= 50:
print(
"The provided token doesn't look a valid Discord bot token."
" Instance's token will remain unchanged.\n"
)
return
red.loop.run_until_complete(red._config.token.set(token))
elif not no_prompt and confirm("Would you like to change instance's token?", default=False):
interactive_config(red, False, True, print_header=False)
print("Token updated.\n")


def _edit_owner(red, owner, no_prompt):
if owner:
if not (15 <= len(str(owner)) <= 21):
print(
"The provided owner id doesn't look like a valid Discord user id."
" Instance's owner will remain unchanged."
)
return
red.loop.run_until_complete(red._config.owner.set(owner))
elif not no_prompt and confirm("Would you like to change instance's owner?", default=False):
print(
"Remember:\n"
"ONLY the person who is hosting Red should be owner."
" This has SERIOUS security implications."
" The owner can access any data that is present on the host system.\n"
)
if confirm("Are you sure you want to change instance's owner?", default=False):
print("Please enter a Discord user id for new owner:")
while True:
owner_id = input("> ").strip()
if not (15 <= len(owner_id) <= 21 and owner_id.isdecimal()):
print("That doesn't look like a valid Discord user id.")
continue
owner_id = int(owner_id)
red.loop.run_until_complete(red._config.owner.set(owner_id))
print("Owner updated.")
break
else:
print("Instance's owner will remain unchanged.")
print()


def _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt):
if new_name:
name = new_name
if name in _get_instance_names() and not confirm_overwrite:
name = old_name
print(
"An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag."
)
elif not no_prompt and confirm("Would you like to change the instance name?", default=False):
name = get_name()
if name in _get_instance_names():
print(
"WARNING: An instance already exists with this name. "
"Continuing will overwrite the existing instance config."
)
if not confirm(
"Are you absolutely certain you want to continue with this instance name?",
default=False,
):
print("Instance name will remain unchanged.")
name = old_name
else:
print("Instance name updated.")
print()
else:
name = old_name
return name


def _edit_data_path(data, data_path, copy_data, no_prompt):
# This modifies the passed dict.
if data_path:
data["DATA_PATH"] = data_path
if copy_data and not _copy_data(data):
print("Can't copy data to non-empty location. Data location will remain unchanged.")
data["DATA_PATH"] = data_manager.basic_config["DATA_PATH"]
elif not no_prompt and confirm("Would you like to change the data location?", default=False):
data["DATA_PATH"] = get_data_dir()
if confirm(
"Do you want to copy the data from old location?", default=True
) and not _copy_data(data):
print("Can't copy the data to non-empty location.")
if not confirm("Do you still want to use the new data location?"):
data["DATA_PATH"] = data_manager.basic_config["DATA_PATH"]
print("Data location will remain unchanged.")
else:
print("Data location updated.")


def _copy_data(data):
if Path(data["DATA_PATH"]).exists():
if any(os.scandir(data["DATA_PATH"])):
return False
else:
# this is needed because copytree doesn't work when destination folder exists
# Python 3.8 has `dirs_exist_ok` option for that
os.rmdir(data["DATA_PATH"])
shutil.copytree(data_manager.basic_config["DATA_PATH"], data["DATA_PATH"])
return True


async def sigterm_handler(red, log):
log.info("SIGTERM received. Quitting...")
await red.shutdown(restart=False)
@@ -79,7 +231,7 @@ def main():
print(description)
print("Current Version: {}".format(__version__))
sys.exit(0)
elif not cli_flags.instance_name and not cli_flags.no_instance:
elif not cli_flags.instance_name and (not cli_flags.no_instance or cli_flags.edit):
print("Error: No instance name was provided!")
sys.exit(1)
if cli_flags.no_instance:
@@ -108,6 +260,16 @@ def main():
cli_flags=cli_flags, description=description, dm_help=None, fetch_offline_members=True
)
loop.run_until_complete(red._maybe_update_config())

if cli_flags.edit:
try:
edit_instance(red, cli_flags)
except (KeyboardInterrupt, EOFError):
print("Aborted!")
finally:
loop.run_until_complete(driver_cls.teardown())
sys.exit(0)

init_global_checks(red)
init_events(red, cli_flags)

@@ -154,8 +316,7 @@ def main():
log.critical("This token doesn't seem to be valid.")
db_token = loop.run_until_complete(red._config.token())
if db_token and not cli_flags.no_prompt:
print("\nDo you want to reset the token? (y/n)")
if confirm("> "):
if confirm("\nDo you want to reset the token?"):
loop.run_until_complete(red._config.token.set(""))
print("Token has been reset.")
except KeyboardInterrupt:
@@ -1,17 +1,42 @@
import argparse
import asyncio
import logging
import sys
from typing import Optional


def confirm(m=""):
return input(m).lower().strip() in ("y", "yes")
def confirm(text: str, default: Optional[bool] = None) -> bool:
if default is None:
options = "y/n"
elif default is True:
options = "Y/n"
elif default is False:
options = "y/N"
else:
raise TypeError(f"expected bool, not {type(default)}")

while True:
try:
value = input(f"{text}: [{options}] ").lower().strip()
except (KeyboardInterrupt, EOFError):
print("\nAborted!")
sys.exit(1)
if value in ("y", "yes"):
return True
if value in ("n", "no"):
return False
if value == "":
if default is not None:
return default
print("Error: invalid input")


def interactive_config(red, token_set, prefix_set):
def interactive_config(red, token_set, prefix_set, *, print_header=True):
loop = asyncio.get_event_loop()
token = ""

print("Red - Discord Bot | Configuration process\n")
if print_header:
print("Red - Discord Bot | Configuration process\n")

if not token_set:
print("Please enter a valid token:")
@@ -35,8 +60,7 @@ def interactive_config(red, token_set, prefix_set):
while not prefix:
prefix = input("Prefix> ")
if len(prefix) > 10:
print("Your prefix seems overly long. Are you sure that it's correct? (y/n)")
if not confirm("> "):
if not confirm("Your prefix seems overly long. Are you sure that it's correct?"):
prefix = ""
if prefix:
loop.run_until_complete(red._config.prefix.set([prefix]))
@@ -54,6 +78,37 @@ def parse_cli_flags(args):
action="store_true",
help="List all instance names setup with 'redbot-setup'",
)
parser.add_argument(
"--edit",
action="store_true",
help="Edit the instance. This can be done without console interaction "
"by passing --no-prompt and arguments that you want to change (available arguments: "
"--edit-instance-name, --edit-data-path, --copy-data, --owner, --token).",
)
parser.add_argument(
"--edit-instance-name",
type=str,
help="New name for the instance. This argument only works with --edit argument passed.",
)
parser.add_argument(
"--overwrite-existing-instance",
action="store_true",
help="Confirm overwriting of existing instance when changing name."
" This argument only works with --edit argument passed.",
)
parser.add_argument(
"--edit-data-path",
type=str,
help=(
"New data path for the instance. This argument only works with --edit argument passed."
),
)
parser.add_argument(
"--copy-data",
action="store_true",
help="Copy data from old location. This argument only works "
"with --edit and --edit-data-path arguments passed.",
)
parser.add_argument(
"--owner",
type=int,
@@ -65,7 +120,7 @@ def parse_cli_flags(args):
"--co-owner",
type=int,
default=[],
nargs="*",
nargs="+",
help="ID of a co-owner. Only people who have access "
"to the system that is hosting Red should be "
"co-owners, as this gives them complete access "
@@ -87,7 +142,7 @@ def parse_cli_flags(args):
parser.add_argument(
"--load-cogs",
type=str,
nargs="*",
nargs="+",
help="Force loading specified cogs from the installed packages. "
"Can be used with the --no-cogs flag to load these cogs exclusively.",
)
@@ -264,7 +264,7 @@ def instance_menu():
print("Cancelling...")
return

if confirm("\nDo you want to create a backup for an instance? (y/n) "):
if confirm("\nDo you want to create a backup for an instance?"):
for index, instance in instances.items():
print("\nRemoving {}...".format(index))
await create_backup(index)

0 comments on commit 1651de1

Please sign in to comment.
You can’t perform that action at this time.