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.") else: print("Instance name updated.") print() else: name = old_name return name
async 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 await 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) await red._config.owner.set(owner_id) print("Owner updated.") break else: print("Instance's owner will remain unchanged.") print()
def get_data_dir(): default_data_dir = Path(appdir.user_data_dir) print("Hello! Before we begin the full configuration process we need to" " gather some initial information about where you'd like us" " to store your bot's data. We've attempted to figure out a" " sane default data location which is printed below. If you don't" " want to change this default please press [ENTER], otherwise" " input your desired data location.") print() print("Default: {}".format(default_data_dir)) new_path = input("> ") if new_path != "": new_path = Path(new_path) default_data_dir = new_path if not default_data_dir.exists(): try: default_data_dir.mkdir(parents=True, exist_ok=True) except OSError: print("We were unable to create your chosen directory." " You may need to restart this process with admin" " privileges.") sys.exit(1) print("You have chosen {} to be your data directory.".format( default_data_dir)) if not confirm("Please confirm (y/n):"): print("Please start the process over.") sys.exit(0) return default_data_dir
def get_name(name: str) -> str: INSTANCE_NAME_RE = re.compile(r"[A-Za-z0-9_\.\-]*") if name: if INSTANCE_NAME_RE.fullmatch(name) is None: print( "ERROR: Instance names can only include characters A-z, numbers, " "underscores (_) and periods (.).") sys.exit(1) return name while len(name) == 0: print("Please enter a name for your instance," " it will be used to run your bot from here on out.\n" "This name is case-sensitive and should only include characters" " A-z, numbers, underscores (_) and periods (.).") name = input("> ") if INSTANCE_NAME_RE.fullmatch(name) is None: print( "ERROR: Instance names can only include characters A-z, numbers, " "underscores (_) and periods (.).") name = "" elif "-" in name and not confirm( "Hyphens (-) in instance names may cause issues. Are you sure you want to continue with this instance name?", default=False, ): name = "" print() # new line for aesthetics return name
async def reset_red(): instances = load_existing_config() if not instances: print("No instance to delete.\n") return print( "WARNING: You are about to remove ALL Bot instances on this computer.") print("If you want to reset data of only one instance, " "please select option 5 in the launcher.") await asyncio.sleep(2) print("\nIf you continue you will remove these instanes.\n") for instance in list(instances.keys()): print(" - {}".format(instance)) await asyncio.sleep(3) print('\nIf you want to reset all instances, type "I agree".') response = input("> ").strip() if response != "I agree": print("Cancelling...") return if confirm("\nDo you want to create a backup for an instance? (y/n) "): for index, instance in instances.items(): print("\nRemoving {}...".format(index)) await create_backup(index, instance) await remove_instance(index, instance) else: for index, instance in instances.items(): await remove_instance(index, instance) print("All instances have been removed.")
async def edit_instance(): instance_list = load_existing_config() if not instance_list: print("No instances have been set up!") return print("You have chosen to edit an instance. The following " "is a list of instances that currently exist:\n") for instance in instance_list.keys(): print("{}\n".format(instance)) print("Please select one of the above by entering its name") selected = input("> ") if selected not in instance_list.keys(): print("That isn't a valid instance!") return instance_data = instance_list[selected] default_dirs = deepcopy(basic_config_default) current_data_dir = Path(instance_data["DATA_PATH"]) print("You have selected '{}' as the instance to modify.".format(selected)) if not confirm("Please confirm (y/n):"): print("Ok, we will not continue then.") return print("Ok, we will continue on.") print() if confirm("Would you like to change the instance name? (y/n)"): name = get_name() else: name = selected if confirm("Would you like to change the data location? (y/n)"): default_data_dir = get_data_dir() default_dirs["DATA_PATH"] = str(default_data_dir.resolve()) else: default_dirs["DATA_PATH"] = str(current_data_dir.resolve()) if name != selected: save_config(selected, {}, remove=True) save_config(name, default_dirs) print("Your basic configuration has been edited")
async 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 await red._config.token.set(token) elif not no_prompt and confirm( "Would you like to change instance's token?", default=False): await interactive_config(red, False, True, print_header=False) print("Token updated.\n")
async def create_backup(instance): instance_vals = instance_data[instance] if confirm( "Would you like to make a backup of the data for this instance? (y/n)" ): load_basic_configuration(instance) if instance_vals["STORAGE_TYPE"] == "MongoDB": await mongo_to_json(instance) print("Backing up the instance's data...") backup_filename = "redv3-{}-{}.tar.gz".format( instance, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")) pth = Path(instance_vals["DATA_PATH"]) if pth.exists(): backup_pth = pth.home() backup_file = backup_pth / backup_filename to_backup = [] exclusions = [ "__pycache__", "Lavalink.jar", os.path.join("Downloader", "lib"), os.path.join("CogManager", "cogs"), os.path.join("RepoManager", "repos"), ] from redbot.cogs.downloader.repo_manager import RepoManager repo_mgr = RepoManager() await repo_mgr.initialize() repo_output = [] for repo in repo_mgr._repos.values(): repo_output.append({ "url": repo.url, "name": repo.name, "branch": repo.branch }) repo_filename = pth / "cogs" / "RepoManager" / "repos.json" with open(str(repo_filename), "w") as f: f.write(json.dumps(repo_output, indent=4)) instance_vals = {instance_name: basic_config} instance_file = pth / "instance.json" with open(str(instance_file), "w") as instance_out: instance_out.write(json.dumps(instance_vals, indent=4)) for f in pth.glob("**/*"): if not any(ex in str(f) for ex in exclusions): to_backup.append(f) with tarfile.open(str(backup_file), "w:gz") as tar: for f in to_backup: tar.add(str(f), recursive=False) print("A backup of {} has been made. It is at {}".format( instance, backup_file))
def save_config(name, data, remove=False): config = load_existing_config() if remove and name in config: config.pop(name) else: if name in config: 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 (y/n)? "): print("Not continuing") sys.exit(0) config[name] = data JsonIO(config_file)._save_json(config)
def _edit_data_path(data, instance_name, data_path, copy_data, no_prompt): # This modifies the passed dict. if data_path: new_path = Path(data_path) try: exists = new_path.exists() except OSError: print("We were unable to check your chosen directory." " Provided path may contain an invalid character." " Data location will remain unchanged.") if not exists: try: new_path.mkdir(parents=True, exist_ok=True) except OSError: print("We were unable to create your chosen directory." " Data location will remain unchanged.") 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(instance_name) if confirm("Do you want to copy the data from old location?", default=True): if 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.") return print("Old data has been copied over to the new location.") print("Data location updated.")
def save_config(name, data, remove=False): config = load_existing_config() if remove and name in config: config.pop(name) else: if name in config: 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 (y/n)? "): print("Not continuing") sys.exit(0) config[name] = data with config_file.open("w", encoding="utf-8") as fs: json.dump(config, fs, indent=4)
async def _edit_prefix(red, prefix, no_prompt): if prefix: prefixes = sorted(prefix, reverse=True) await red._config.prefix.set(prefixes) elif not no_prompt and confirm("Would you like to change instance's prefixes?", default=False): print( "Enter the prefixes, separated by a space (please note " "that prefixes containing a space will need to be added with [p]set prefix)" ) while True: prefixes = input("> ").strip().split() if not prefixes: print("You need to pass at least one prefix!") continue prefixes = sorted(prefixes, reverse=True) await red._config.prefix.set(prefixes) print("Prefixes updated.\n") break
async def run_red_bot(log, red: Red, cli_flags: Namespace) -> None: """ This runs the bot. Any shutdown which is a result of not being able to log in needs to raise a SystemExit exception. If the bot starts normally, the bot should be left to handle the exit case. It will raise SystemExit in a task, which will reach the event loop and interrupt running forever, then trigger our cleanup process, and does not need additional handling in this function. """ driver_cls = drivers.get_driver_class() await driver_cls.initialize(**data_manager.storage_details()) init_logging(level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs") # log.debug("====Basic Config====") # log.debug("Data Path: %s", data_manager._base_data_path()) # log.debug("Storage Type: %s", data_manager.storage_type()) # lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061) # We might want to change handling of requirements in Downloader at later date LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib" LIB_PATH.mkdir(parents=True, exist_ok=True) if str(LIB_PATH) not in sys.path: sys.path.append(str(LIB_PATH)) # "It's important to note that the global `working_set` object is initialized from # `sys.path` when `pkg_resources` is first imported, but is only updated if you do # all future `sys.path` manipulation via `pkg_resources` APIs. If you manually modify # `sys.path`, you must invoke the appropriate methods on the `working_set` instance # to keep it in sync." # Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects pkg_resources.working_set.add_entry(str(LIB_PATH)) sys.meta_path.insert(0, SharedLibImportWarner()) if cli_flags.token: token = cli_flags.token else: token = os.environ.get("RED_TOKEN", None) if not token: token = await red._config.token() prefix = cli_flags.prefix or await red._config.prefix() if not (token and prefix): if cli_flags.no_prompt is False: new_token = await interactive_config(red, token_set=bool(token), prefix_set=bool(prefix)) if new_token: token = new_token else: log.critical("Token and prefix must be set in order to login.") sys.exit(1) if cli_flags.dry_run: await red.http.close() sys.exit(0) try: await red.start(token, bot=True, cli_flags=cli_flags) except discord.LoginFailure: log.critical("This token doesn't seem to be valid.") db_token = await red._config.token() if db_token and not cli_flags.no_prompt: if confirm("\nDo you want to reset the token?"): await red._config.token.set("") print("Token has been reset.") sys.exit(0) sys.exit(1) except Exception: log.exception("hmm") raise return None
async def run_bot(red: Red, cli_flags: Namespace) -> None: """ This runs the bot. Any shutdown which is a result of not being able to log in needs to raise a SystemExit exception. If the bot starts normally, the bot should be left to handle the exit case. It will raise SystemExit in a task, which will reach the event loop and interrupt running forever, then trigger our cleanup process, and does not need additional handling in this function. """ driver_cls = drivers.get_driver_class() await driver_cls.initialize(**data_manager.storage_details()) redbot.logging.init_logging( level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs", cli_flags=cli_flags, ) log.debug("====Basic Config====") log.debug("Data Path: %s", data_manager._base_data_path()) log.debug("Storage Type: %s", data_manager.storage_type()) # lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061) # We might want to change handling of requirements in Downloader at later date LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib" LIB_PATH.mkdir(parents=True, exist_ok=True) if str(LIB_PATH) not in sys.path: sys.path.append(str(LIB_PATH)) # "It's important to note that the global `working_set` object is initialized from # `sys.path` when `pkg_resources` is first imported, but is only updated if you do # all future `sys.path` manipulation via `pkg_resources` APIs. If you manually modify # `sys.path`, you must invoke the appropriate methods on the `working_set` instance # to keep it in sync." # Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects pkg_resources.working_set.add_entry(str(LIB_PATH)) sys.meta_path.insert(0, SharedLibImportWarner()) if cli_flags.token: token = cli_flags.token else: token = os.environ.get("RED_TOKEN", None) if not token: token = await red._config.token() prefix = cli_flags.prefix or await red._config.prefix() if not (token and prefix): if cli_flags.no_prompt is False: new_token = await interactive_config(red, token_set=bool(token), prefix_set=bool(prefix)) if new_token: token = new_token else: log.critical("Token and prefix must be set in order to login.") sys.exit(1) if cli_flags.dry_run: await red.http.close() sys.exit(0) try: await red.start(token, bot=True) except discord.LoginFailure: log.critical("This token doesn't seem to be valid.") db_token = await red._config.token() if db_token and not cli_flags.no_prompt: if confirm("\nDo you want to reset the token?"): await red._config.token.set("") print("Token has been reset.") sys.exit(0) sys.exit(1) except discord.PrivilegedIntentsRequired: console = rich.get_console() console.print( "Red requires all Privileged Intents to be enabled.\n" "You can find out how to enable Privileged Intents with this guide:\n" "https://docs.discord.red/en/stable/bot_application_guide.html#enabling-privileged-intents", style="red", ) sys.exit(1) except _NoOwnerSet: print( "Bot doesn't have any owner set!\n" "This can happen when your bot's application is owned by team" " as team members are NOT owners by default.\n\n" "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" "With that out of the way, depending on who you want to be considered as owner," " you can:\n" "a) pass --team-members-are-owners when launching Red" " - in this case Red will treat all members of the bot application's team as owners\n" f"b) set owner manually with `redbot --edit {cli_flags.instance_name}`\n" "c) pass owner ID(s) when launching Red with --owner" " (and --co-owner if you need more than one) flag\n") sys.exit(1) return None
async def run_bot(red: Red, cli_flags: Namespace) -> None: """ This runs the bot. Any shutdown which is a result of not being able to log in needs to raise a SystemExit exception. If the bot starts normally, the bot should be left to handle the exit case. It will raise SystemExit in a task, which will reach the event loop and interrupt running forever, then trigger our cleanup process, and does not need additional handling in this function. """ driver_cls = drivers.get_driver_class() await driver_cls.initialize(**data_manager.storage_details()) redbot.logging.init_logging( level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs" ) log.debug("====Basic Config====") log.debug("Data Path: %s", data_manager._base_data_path()) log.debug("Storage Type: %s", data_manager.storage_type()) # lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061) # We might want to change handling of requirements in Downloader at later date LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib" LIB_PATH.mkdir(parents=True, exist_ok=True) if str(LIB_PATH) not in sys.path: sys.path.append(str(LIB_PATH)) sys.meta_path.insert(0, SharedLibImportWarner()) if cli_flags.token: token = cli_flags.token else: token = os.environ.get("RED_TOKEN", None) if not token: token = await red._config.token() prefix = cli_flags.prefix or await red._config.prefix() if not (token and prefix): if cli_flags.no_prompt is False: new_token = await interactive_config( red, token_set=bool(token), prefix_set=bool(prefix) ) if new_token: token = new_token else: log.critical("Token and prefix must be set in order to login.") sys.exit(1) if cli_flags.dry_run: await red.http.close() sys.exit(0) try: await red.start(token, bot=True, cli_flags=cli_flags) except discord.LoginFailure: log.critical("This token doesn't seem to be valid.") db_token = await red._config.token() if db_token and not cli_flags.no_prompt: if confirm("\nDo you want to reset the token?"): await red._config.token.set("") print("Token has been reset.") sys.exit(0) sys.exit(1) return None
async def edit_instance(): instance_list = load_existing_config() if not instance_list: print("No instances have been set up!") return print("You have chosen to edit an instance. The following " "is a list of instances that currently exist:\n") for instance in instance_list.keys(): print("{}\n".format(instance)) print("Please select one of the above by entering its name") selected = input("> ") if selected not in instance_list.keys(): print("That isn't a valid instance!") return instance_data = instance_list[selected] default_dirs = deepcopy(basic_config_default) current_data_dir = Path(instance_data["DATA_PATH"]) print("You have selected '{}' as the instance to modify.".format(selected)) if not confirm("Please confirm (y/n):"): print("Ok, we will not continue then.") return print("Ok, we will continue on.") print() if confirm("Would you like to change the instance name? (y/n)"): name = get_name() else: name = selected if confirm("Would you like to change the data location? (y/n)"): default_data_dir = get_data_dir() default_dirs["DATA_PATH"] = str(default_data_dir.resolve()) else: default_dirs["DATA_PATH"] = str(current_data_dir.resolve()) if confirm("Would you like to change the storage type? (y/n):"): storage = get_storage_type() storage_dict = {1: "JSON", 2: "MongoDB"} default_dirs["STORAGE_TYPE"] = storage_dict[storage] if storage_dict.get(storage, 1) == "MongoDB": from redbot.core.drivers.red_mongo import get_config_details storage_details = get_config_details() default_dirs["STORAGE_DETAILS"] = storage_details if instance_data["STORAGE_TYPE"] == "JSON": if confirm("Would you like to import your data? (y/n) "): await json_to_mongo(current_data_dir, storage_details) else: storage_details = instance_data["STORAGE_DETAILS"] default_dirs["STORAGE_DETAILS"] = {} if instance_data["STORAGE_TYPE"] == "MongoDB": if confirm("Would you like to import your data? (y/n) "): await mongo_to_json(current_data_dir, storage_details) if name != selected: save_config(selected, {}, remove=True) save_config(name, default_dirs) print("Your basic configuration has been edited")
def main(): description = "Bot Base - Version {}".format(__version__) cli_flags = parse_cli_flags(sys.argv[1:]) if cli_flags.list_instances: list_instances() elif cli_flags.version: print(description) sys.exit(0) elif not cli_flags.instance_name and not cli_flags.no_instance: print("Error: No instance name was provided!") sys.exit(1) if cli_flags.no_instance: print( "\033[1m" "Warning: The data will be placed in a temporary folder and removed on next system " "reboot." "\033[0m") cli_flags.instance_name = "temporary_red" create_temp_config() load_basic_configuration(cli_flags.instance_name) log = init_loggers(cli_flags) loop = asyncio.get_event_loop() red = Red(cli_flags=cli_flags, description=description, pm_help=None) init_global_checks(red) init_events(red, cli_flags) loop.run_until_complete(red.cog_mgr.initialize()) red.add_cog(Core(red)) red.add_cog(CogManagerUI()) if cli_flags.dev: red.add_cog(Dev()) # noinspection PyProtectedMember modlog._init() # noinspection PyProtectedMember bank._init() if os.name == "posix": loop.add_signal_handler( SIGTERM, lambda: asyncio.ensure_future(sigterm_handler(red, log))) tmp_data = {} loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) token = os.environ.get("RED_TOKEN", tmp_data["token"]) if cli_flags.token: token = cli_flags.token prefix = cli_flags.prefix or tmp_data["prefix"] if not (token and prefix): if cli_flags.no_prompt is False: new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix)) if new_token: token = new_token else: log.critical("Token and prefix must be set in order to login.") sys.exit(1) loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) if cli_flags.dry_run: loop.run_until_complete(red.http.close()) sys.exit(0) try: loop.run_until_complete(red.start(token, bot=True)) except discord.LoginFailure: log.critical("This token doesn't seem to be valid.") db_token = loop.run_until_complete(red.db.token()) if db_token and not cli_flags.no_prompt: print("\nDo you want to reset the token? (y/n)") if confirm("> "): loop.run_until_complete(red.db.token.set("")) print("Token has been reset.") except KeyboardInterrupt: log.info("Keyboard interrupt detected. Quitting...") loop.run_until_complete(red.logout()) red._shutdown_mode = ExitCodes.SHUTDOWN except Exception as e: log.critical("Fatal exception", exc_info=e) loop.run_until_complete(red.logout()) finally: pending = asyncio.Task.all_tasks(loop=red.loop) gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True) gathered.cancel() try: loop.run_until_complete(red.rpc.close()) except AttributeError: pass sys.exit(red._shutdown_mode.value)
async def run_bot(red: Red, cli_flags: Namespace): driver_cls = drivers.get_driver_class() await driver_cls.initialize(**data_manager.storage_details()) redbot.logging.init_logging( level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs" ) log.debug("====Basic Config====") log.debug("Data Path: %s", data_manager._base_data_path()) log.debug("Storage Type: %s", data_manager.storage_type()) if cli_flags.edit: try: await edit_instance(red, cli_flags) except (KeyboardInterrupt, EOFError): print("Aborted!") finally: await driver_cls.teardown() sys.exit(0) # lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061) # We might want to change handling of requirements in Downloader at later date LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib" LIB_PATH.mkdir(parents=True, exist_ok=True) if str(LIB_PATH) not in sys.path: sys.path.append(str(LIB_PATH)) sys.meta_path.insert(0, SharedLibImportWarner()) if cli_flags.token: token = cli_flags.token else: token = os.environ.get("RED_TOKEN", None) if not token: token = await red._config.token() prefix = cli_flags.prefix or await red._config.prefix() if not (token and prefix): if cli_flags.no_prompt is False: new_token = await interactive_config( red, token_set=bool(token), prefix_set=bool(prefix) ) if new_token: token = new_token else: log.critical("Token and prefix must be set in order to login.") sys.exit(1) if cli_flags.dry_run: await red.http.close() sys.exit(0) try: await red.start(token, bot=True, cli_flags=cli_flags) except discord.LoginFailure: log.critical("This token doesn't seem to be valid.") db_token = await red._config.token() if db_token and not cli_flags.no_prompt: if confirm("\nDo you want to reset the token?"): await red._config.token.set("") print("Token has been reset.")