diff --git a/cogs/mod.py b/cogs/mod.py index e194c49a7..911b536e0 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -1378,7 +1378,7 @@ def setup(bot): global logger check_folders() check_files() - logger = logging.getLogger("mod") + logger = logging.getLogger("red.mod") # Prevents the logger from being loaded again in case of module reload if logger.level == 0: logger.setLevel(logging.INFO) diff --git a/cogs/owner.py b/cogs/owner.py index 116c55e39..78a0e67fd 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -225,13 +225,16 @@ class Owner: result = str(result) if not ctx.message.channel.is_private: - censor = (self.bot.settings.email, self.bot.settings.password) + censor = (self.bot.settings.email, + self.bot.settings.password, + self.bot.settings.token) r = "[EXPUNGED]" for w in censor: - if w != "": - result = result.replace(w, r) - result = result.replace(w.lower(), r) - result = result.replace(w.upper(), r) + if w is None or w == "": + continue + result = result.replace(w, r) + result = result.replace(w.lower(), r) + result = result.replace(w.upper(), r) result = list(pagify(result, shorten_by=16)) @@ -263,11 +266,16 @@ class Owner: @_set.command(pass_context=True) async def owner(self, ctx): """Sets owner""" + if self.bot.settings.no_prompt is True: + await self.bot.say("Console interaction is disabled. Start Red " + "without the `--no-prompt` flag to use this " + "command.") + return if self.setowner_lock: await self.bot.say("A set owner command is already pending.") return - if self.bot.settings.owner != "id_here": + if self.bot.settings.owner is not None: await self.bot.say( "The owner is already set. Remember that setting the owner " "to someone else other than who hosts the bot has security " @@ -294,6 +302,7 @@ class Owner: return self.bot.settings.prefixes = sorted(prefixes, reverse=True) + self.bot.settings.save_settings() log.debug("Setting global prefixes to:\n\t{}" "".format(self.bot.settings.prefixes)) @@ -315,6 +324,7 @@ class Owner: if prefixes == (): self.bot.settings.set_server_prefixes(server, []) + self.bot.settings.save_settings() current_p = ", ".join(self.bot.settings.prefixes) await self.bot.say("Server prefixes reset. Current prefixes: " "`{}`".format(current_p)) @@ -322,6 +332,7 @@ class Owner: prefixes = sorted(prefixes, reverse=True) self.bot.settings.set_server_prefixes(server, prefixes) + self.bot.settings.save_settings() log.debug("Setting server's {} prefixes to:\n\t{}" "".format(server.id, self.bot.settings.prefixes)) @@ -472,9 +483,8 @@ class Owner: if len(token) < 50: await self.bot.say("Invalid token.") else: - self.bot.settings.login_type = "token" - self.bot.settings.email = token - self.bot.settings.password = "" + self.bot.settings.token = token + self.bot.settings.save_settings() await self.bot.say("Token set. Restart me.") log.debug("Token changed.") @@ -645,7 +655,7 @@ class Owner: @commands.command(pass_context=True) async def contact(self, ctx, *, message : str): """Sends message to the owner""" - if self.bot.settings.owner == "id_here": + if self.bot.settings.owner is None: await self.bot.say("I have no owner set.") return owner = discord.utils.get(self.bot.get_all_members(), @@ -684,7 +694,7 @@ class Owner: py_version = "[{}.{}.{}]({})".format(*os.sys.version_info[:3], python_url) - owner_set = self.bot.settings.owner != "id_here" + owner_set = self.bot.settings.owner is not None owner = self.bot.settings.owner if owner_set else None if owner: owner = discord.utils.get(self.bot.get_all_members(), id=owner) @@ -782,6 +792,7 @@ class Owner: if choice == "yes": self.bot.settings.owner = author.id + self.bot.settings.save_settings() print(author.name + " has been set as owner.") self.setowner_lock = False self.owner.hidden = True diff --git a/cogs/utils/settings.py b/cogs/utils/settings.py index 1e90acf31..69c52c87d 100644 --- a/cogs/utils/settings.py +++ b/cogs/utils/settings.py @@ -1,6 +1,9 @@ from .dataIO import dataIO +from copy import deepcopy import discord import os +import argparse + default_path = "data/red/settings.json" @@ -11,14 +14,19 @@ class Settings: self.path = path self.check_folders() self.default_settings = { - "EMAIL": "EmailHere", "PASSWORD": "", "OWNER": "id_here", + "TOKEN": None, + "EMAIL": None, + "PASSWORD": None, + "OWNER": None, "PREFIXES": [], "default": {"ADMIN_ROLE": "Transistor", "MOD_ROLE": "Process", - "PREFIXES": []}, - "LOGIN_TYPE": "email"} + "PREFIXES": []} + } + self.memory_only = False + if not dataIO.is_valid_json(self.path): - self.bot_settings = self.default_settings + self.bot_settings = deepcopy(self.default_settings) self.save_settings() else: current = dataIO.load_json(self.path) @@ -30,8 +38,58 @@ class Settings: " field to red settings.json") dataIO.save_json(self.path, current) self.bot_settings = dataIO.load_json(self.path) + if "default" not in self.bot_settings: - self.update_old_settings() + self.update_old_settings_v1() + + if "LOGIN_TYPE" in self.bot_settings: + self.update_old_settings_v2() + + self.parse_cmd_arguments() + + def parse_cmd_arguments(self): + 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("--admin-role", help="Role seen as admin role by " + "Red") + parser.add_argument("--mod-role", help="Role seen as mod role by Red") + 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("--self-bot", + action='store_true', + help="Specifies if Red should log in as selfbot") + parser.add_argument("--memory-only", + action="store_true", + help="Arguments passed and future edits to the " + "settings will not be saved to disk") + parser.add_argument("--debug", + action="store_true", + help="Enables debug mode") + + args = parser.parse_args() + + if args.owner: + self.owner = args.owner + if args.prefix: + self.prefixes = sorted(args.prefix, reverse=True) + if args.admin_role: + self.default_admin = args.admin_role + if args.mod_role: + self.default_mod = args.mod_role + + self.no_prompt = args.no_prompt + self.self_bot = args.self_bot + self.memory_only = args.memory_only + self.debug = args.debug + + self.save_settings() def check_folders(self): folders = ("data", os.path.dirname(self.path), "cogs", "cogs/utils") @@ -41,16 +99,35 @@ class Settings: os.makedirs(folder) def save_settings(self): - dataIO.save_json(self.path, self.bot_settings) + if not self.memory_only: + dataIO.save_json(self.path, self.bot_settings) - def update_old_settings(self): + def update_old_settings_v1(self): + # This converts the old settings format mod = self.bot_settings["MOD_ROLE"] admin = self.bot_settings["ADMIN_ROLE"] del self.bot_settings["MOD_ROLE"] del self.bot_settings["ADMIN_ROLE"] self.bot_settings["default"] = {"MOD_ROLE": mod, "ADMIN_ROLE": admin, - "PREFIXES" : []} + "PREFIXES": [] + } + self.save_settings() + + def update_old_settings_v2(self): + # The joys of backwards compatibility + settings = self.bot_settings + if settings["EMAIL"] == "EmailHere": + settings["EMAIL"] = None + if settings["PASSWORD"] == "": + settings["PASSWORD"] = None + if settings["LOGIN_TYPE"] == "token": + settings["TOKEN"] = settings["EMAIL"] + settings["EMAIL"] = None + settings["PASSWORD"] = None + else: + settings["TOKEN"] = None + del settings["LOGIN_TYPE"] self.save_settings() @property @@ -60,25 +137,42 @@ class Settings: @owner.setter def owner(self, value): self.bot_settings["OWNER"] = value - self.save_settings() + + @property + def token(self): + return os.environ.get("RED_TOKEN", self.bot_settings["TOKEN"]) + + @token.setter + def token(self, value): + self.bot_settings["TOKEN"] = value + self.bot_settings["EMAIL"] = None + self.bot_settings["PASSWORD"] = None @property def email(self): - return self.bot_settings["EMAIL"] + return os.environ.get("RED_EMAIL", self.bot_settings["EMAIL"]) @email.setter def email(self, value): self.bot_settings["EMAIL"] = value - self.save_settings() + self.bot_settings["TOKEN"] = None @property def password(self): - return self.bot_settings["PASSWORD"] + return os.environ.get("RED_PASSWORD", self.bot_settings["PASSWORD"]) @password.setter def password(self, value): self.bot_settings["PASSWORD"] = value - self.save_settings() + + @property + def login_credentials(self): + if self.token: + return (self.token,) + elif self.email and self.password: + return (self.email, self.password) + else: + return tuple() @property def prefixes(self): @@ -88,7 +182,6 @@ class Settings: def prefixes(self, value): assert isinstance(value, list) self.bot_settings["PREFIXES"] = value - self.save_settings() @property def default_admin(self): @@ -101,20 +194,18 @@ class Settings: if "default" not in self.bot_settings: self.update_old_settings() self.bot_settings["default"]["ADMIN_ROLE"] = value - self.save_settings() @property def default_mod(self): if "default" not in self.bot_settings: - self.update_old_settings() + self.update_old_settings_v1() return self.bot_settings["default"].get("MOD_ROLE", "") @default_mod.setter def default_mod(self, value): if "default" not in self.bot_settings: - self.update_old_settings() + self.update_old_settings_v1() self.bot_settings["default"]["MOD_ROLE"] = value - self.save_settings() @property def servers(self): @@ -125,15 +216,6 @@ class Settings: ret.update({server: self.bot_settings[server]}) return ret - @property - def login_type(self): - return self.bot_settings["LOGIN_TYPE"] - - @login_type.setter - def login_type(self, value): - self.bot_settings["LOGIN_TYPE"] = value - self.save_settings() - def get_server(self, server): if server is None: return self.bot_settings["default"].copy() diff --git a/red.py b/red.py index 3a40077e3..9e3a5e79b 100644 --- a/red.py +++ b/red.py @@ -1,10 +1,8 @@ import asyncio import os -import time import sys import logging import logging.handlers -import shutil import traceback import datetime @@ -29,6 +27,7 @@ from cogs.utils.settings import Settings from cogs.utils.dataIO import dataIO from cogs.utils.chat_formatting import inline from collections import Counter +from io import TextIOWrapper # # Red, a Discord bot by Twentysix, based on discord.py and its command @@ -61,9 +60,11 @@ class Bot(commands.Bot): return bot.settings.get_prefixes(message.server) self.counter = Counter() - self.uptime = datetime.datetime.now() + self.uptime = datetime.datetime.now() # Will be refreshed before login self._message_modifiers = [] self.settings = Settings() + self._intro_displayed = False + kwargs["self_bot"] = self.settings.self_bot super().__init__(*args, command_prefix=prefix_manager, **kwargs) async def send_message(self, *args, **kwargs): @@ -119,20 +120,23 @@ class Bot(commands.Bot): async def send_cmd_help(self, ctx): if ctx.invoked_subcommand: - pages = bot.formatter.format_help_for(ctx, ctx.invoked_subcommand) + pages = self.formatter.format_help_for(ctx, ctx.invoked_subcommand) for page in pages: - await bot.send_message(ctx.message.channel, page) + await self.send_message(ctx.message.channel, page) else: - pages = bot.formatter.format_help_for(ctx, ctx.command) + pages = self.formatter.format_help_for(ctx, ctx.command) for page in pages: - await bot.send_message(ctx.message.channel, page) + await self.send_message(ctx.message.channel, page) def user_allowed(self, message): author = message.author - if author.bot or author == self.user: + if author.bot: return False + if author == self.user: + return self.settings.self_bot + mod = self.get_cog('Mod') if mod is not None: @@ -195,34 +199,62 @@ settings = bot.settings @bot.event async def on_ready(): + if bot._intro_displayed: + return + bot._intro_displayed = True + owner_cog = bot.get_cog('Owner') total_cogs = len(owner_cog._list_cogs()) users = len(set(bot.get_all_members())) servers = len(bot.servers) channels = len([c for c in bot.get_all_channels()]) - if settings.login_type == "token" and settings.owner == "id_here": - await set_bot_owner() - print('------') - print("{} is now online.".format(bot.user.name)) - print('------') - print("Connected to:") + + login_time = datetime.datetime.now() - bot.uptime + login_time = login_time.seconds + login_time.microseconds/1E6 + + print("Login successful. ({}ms)\n".format(login_time)) + + owner = await set_bot_owner() + + print("-----------------") + print("Red - Discord Bot") + print("-----------------") + print(str(bot.user)) + print("\nConnected to:") print("{} servers".format(servers)) print("{} channels".format(channels)) - print("{} users".format(users)) - print("\n{}/{} active cogs with {} commands".format( - len(bot.cogs), total_cogs, len(bot.commands))) + print("{} users\n".format(users)) prefix_label = "Prefixes:" if len(settings.prefixes) > 1 else "Prefix:" - print("{} {}\n".format(prefix_label, " ".join(settings.prefixes))) - if settings.login_type == "token": - print("------") - print("Use this url to bring your bot to a server:") + print("{} {}".format(prefix_label, " ".join(settings.prefixes))) + print("Owner: " + str(owner)) + print("{}/{} active cogs with {} commands".format( + len(bot.cogs), total_cogs, len(bot.commands))) + print("-----------------") + + if settings.token and not settings.self_bot: + print("\nUse this url to bring your bot to a server:") url = await get_oauth_url() bot.oauth_url = url print(url) - print("------") + + print("\nOfficial server: https://discord.me/Red-DiscordBot") + + if os.name == "nt" and os.path.isfile("update.bat"): + print("\nMake sure to keep your bot updated by running the file " + "update.bat") + else: + print("\nMake sure to keep your bot updated by using: git pull") + print("and: pip3 install -U git+https://github.com/Rapptz/" + "discord.py@master#egg=discord.py[voice]") + await bot.get_cog('Owner').disable_commands() +@bot.event +async def on_resumed(): + bot.counter["session_resumed"] += 1 + + @bot.event async def on_command(command, ctx): bot.counter["processed_commands"] += 1 @@ -271,13 +303,34 @@ async def get_oauth_url(): async def set_bot_owner(): - try: - data = await bot.application_info() - settings.owner = data.owner.id - except Exception as e: - print("Couldn't retrieve owner's ID. Error: {}".format(e)) - return - print("{} has been recognized and set as owner.".format(data.owner.name)) + if settings.self_bot: + settings.owner = bot.user.id + return "[Selfbot mode]" + + if bot.settings.owner: + owner = discord.utils.get(bot.get_all_members(), + id=bot.settings.owner) + if not owner: + try: + owner = await bot.get_user_info(bot.settings.owner) + except: + owner = None + else: + owner = bot.settings.owner # Just the ID then + return owner + + how_to = "Do `[p]set owner` in chat to set it" + + if bot.user.bot: # Can fetch owner + try: + data = await bot.application_info() + settings.owner = data.owner.id + settings.save_settings() + return data.owner + except: + return "Failed to fetch owner. " + how_to + else: + return "Yet to be set. " + how_to def check_folders(): @@ -288,30 +341,30 @@ def check_folders(): os.makedirs(folder) -def check_configs(): - if settings.bot_settings == settings.default_settings: +def interactive_setup(): + first_run = settings.bot_settings == settings.default_settings + + if first_run: print("Red - First run configuration\n") print("If you haven't already, create a new account:\n" "https://twentysix26.github.io/Red-Docs/red_guide_bot_accounts/" "#creating-a-new-bot-account") print("and obtain your bot's token like described.") + + if not settings.login_credentials: print("\nInsert your bot's token:") + while settings.token is None and settings.email is None: + choice = input("> ") + if "@" not in choice and len(choice) >= 50: # Assuming token + settings.token = choice + elif "@" in choice: + settings.email = choice + settings.password = input("\nPassword> ") + else: + print("That doesn't look like a valid token.") + settings.save_settings() - choice = input("> ") - - if "@" not in choice and len(choice) >= 50: # Assuming token - settings.login_type = "token" - settings.email = choice - elif "@" in choice: - settings.login_type = "email" - settings.email = choice - settings.password = input("\nPassword> ") - else: - os.remove('data/red/settings.json') - input("Invalid input. Restart Red and repeat the configuration " - "process.") - exit(1) - + if not settings.prefixes: print("\nChoose a prefix. A prefix is what you type before a command." "\nA typical prefix would be the exclamation mark.\n" "Can be multiple characters. You will be able to change it " @@ -324,30 +377,17 @@ def check_configs(): "\nType yes to confirm or no to change it".format( new_prefix)) confirmation = get_answer() - settings.prefixes = [new_prefix] - if settings.login_type == "email": - print("\nOnce you're done with the configuration, you will have to" - " type '{}set owner' *in Discord's chat*\nto set yourself as" - " owner.\nPress enter to continue".format(new_prefix)) - settings.owner = input("") # Shh, they will never know it's here - if settings.owner == "": - settings.owner = "id_here" - if not settings.owner.isdigit() or len(settings.owner) < 17: - if settings.owner != "id_here": - print("\nERROR: What you entered is not a valid ID. Set " - "yourself as owner later with {}set owner".format( - new_prefix)) - settings.owner = "id_here" - else: - settings.owner = "id_here" + settings.save_settings() + if first_run: print("\nInput the admin role's name. Anyone with this role in Discord" " will be able to use the bot's admin commands") print("Leave blank for default name (Transistor)") settings.default_admin = input("\nAdmin role> ") if settings.default_admin == "": settings.default_admin = "Transistor" + settings.save_settings() print("\nInput the moderator role's name. Anyone with this role in" " Discord will be able to use the bot's mod commands") @@ -355,29 +395,19 @@ def check_configs(): settings.default_mod = input("\nModerator role> ") if settings.default_mod == "": settings.default_mod = "Process" + settings.save_settings() print("\nThe configuration is done. Leave this window always open to" " keep Red online.\nAll commands will have to be issued through" - " Discord's chat, *this window will now be read only*.\nPress" - " enter to continue") + " Discord's chat, *this window will now be read only*.\n" + "Please read this guide for a good overview on how Red works:\n" + "https://twentysix26.github.io/Red-Docs/red_getting_started/\n" + "Press enter to continue") input("\n") - if not os.path.isfile("data/red/cogs.json"): - print("Creating new cogs.json...") - dataIO.save_json("data/red/cogs.json", {}) - def set_logger(): global logger - logger = logging.getLogger("discord") - logger.setLevel(logging.WARNING) - handler = logging.FileHandler( - filename='data/red/discord.log', encoding='utf-8', mode='a') - handler.setFormatter(logging.Formatter( - '%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: ' - '%(message)s', - datefmt="[%d/%m/%Y %H:%M]")) - logger.addHandler(handler) logger = logging.getLogger("red") logger.setLevel(logging.INFO) @@ -389,7 +419,12 @@ def set_logger(): stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(red_format) - stdout_handler.setLevel(logging.INFO) + if settings.debug: + stdout_handler.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + else: + stdout_handler.setLevel(logging.INFO) + logger.setLevel(logging.INFO) fhandler = logging.handlers.RotatingFileHandler( filename='data/red/red.log', encoding='utf-8', mode='a', @@ -399,6 +434,19 @@ def set_logger(): logger.addHandler(fhandler) logger.addHandler(stdout_handler) + dpy_logger = logging.getLogger("discord") + if settings.debug: + dpy_logger.setLevel(logging.DEBUG) + else: + dpy_logger.setLevel(logging.WARNING) + handler = logging.FileHandler( + filename='data/red/discord.log', encoding='utf-8', mode='a') + handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: ' + '%(message)s', + datefmt="[%d/%m/%Y %H:%M]")) + dpy_logger.addHandler(handler) + def ensure_reply(msg): choice = "" @@ -425,7 +473,8 @@ def set_cog(cog, value): def load_cogs(): - no_prompt = "--no-prompt" in sys.argv[1:] + defaults = ("alias", "audio", "customcom", "downloader", "economy", + "general", "image", "mod", "streams", "trivia") try: registry = dataIO.load_json("data/red/cogs.json") @@ -435,87 +484,59 @@ def load_cogs(): bot.load_extension('cogs.owner') owner_cog = bot.get_cog('Owner') if owner_cog is None: - print("You got rid of the damn OWNER cog, it has special functions" - " that I require to run.\n\n" - "I can't start without it!") - print() - print("Go here to find a new copy:\n{}".format( - "https://github.com/Twentysix26/Red-DiscordBot")) + print("The owner cog is missing. It contains core functions without " + "which Red cannot function. Reinstall.") exit(1) failed = [] extensions = owner_cog._list_cogs() + + if not registry: # All default cogs enabled by default + for ext in defaults: + registry["cogs." + ext] = True + for extension in extensions: if extension.lower() == "cogs.owner": continue - in_reg = extension in registry - if in_reg is False: - if no_prompt is True: + to_load = registry.get(extension, False) + if to_load: + try: + owner_cog._load_cog(extension) + except Exception as e: + print("{}: {}".format(e.__class__.__name__, str(e))) + logger.exception(e) + failed.append(extension) registry[extension] = False - continue - print("\nNew extension: {}".format(extension)) - print("Load it?(y/n)") - if not get_answer(): - registry[extension] = False - continue - registry[extension] = True - if not registry[extension]: - continue - try: - owner_cog._load_cog(extension) - except Exception as e: - print("{}: {}".format(e.__class__.__name__, str(e))) - logger.exception(e) - failed.append(extension) - registry[extension] = False - if extensions: - dataIO.save_json("data/red/cogs.json", registry) + dataIO.save_json("data/red/cogs.json", registry) if failed: - print("\nFailed to load: ", end="") - for m in failed: - print(m + " ", end="") - print("\n") - - return owner_cog + print("\nFailed to load: {}\n".format(" ".join(failed))) def main(): - global settings - check_folders() - check_configs() - set_logger() - owner_cog = load_cogs() - if settings.prefixes == []: - print("No prefix set. Defaulting to !") - settings.prefixes = ["!"] - if settings.owner != "id_here": - print("Use !set prefix to set it.") - else: - print("Once you're owner use !set prefix to set it.") - if settings.owner == "id_here" and settings.login_type == "email": - print("Owner has not been set yet. Do '{}set owner' in chat to set " - "yourself as owner.".format(settings.prefixes[0])) + if not settings.no_prompt: + interactive_setup() + load_cogs() + + print("Logging into Discord...") + bot.uptime = datetime.datetime.now() + + if settings.login_credentials: + yield from bot.login(*settings.login_credentials, + bot=not settings.self_bot) else: - owner_cog.owner.hidden = True # Hides the set owner command from help - print("-- Logging in.. --") - if os.name == "nt" and os.path.isfile("update.bat"): - print("Make sure to keep your bot updated by running the file " - "update.bat") - else: - print("Make sure to keep your bot updated by using: git pull") - print("and: pip3 install -U git+https://github.com/Rapptz/" - "discord.py@master#egg=discord.py[voice]") - print("Official server: https://discord.me/Red-DiscordBot") - if settings.login_type == "token": - yield from bot.login(settings.email) - else: - yield from bot.login(settings.email, settings.password) + print("No credentials available to login.") + raise RuntimeError() yield from bot.connect() if __name__ == '__main__': + sys.stdout = TextIOWrapper(sys.stdout.detach(), + encoding=sys.stdout.encoding, + errors="replace", + line_buffering=True) + set_logger() error = False loop = asyncio.get_event_loop() try: @@ -523,16 +544,18 @@ if __name__ == '__main__': except discord.LoginFailure: error = True logger.error(traceback.format_exc()) - choice = input("Invalid login credentials. " - "If they worked before Discord might be having temporary " - "technical issues.\nIn this case, press enter and " - "try again later.\nOtherwise you can type 'reset' to " - "delete the current configuration and redo the setup process " - "again the next start.\n> ") - if choice.strip() == "reset": - shutil.copy('data/red/settings.json', - 'data/red/settings-{}.bak'.format(int(time.time()))) - os.remove('data/red/settings.json') + if not settings.no_prompt: + choice = input("Invalid login credentials. " + "If they worked before Discord might be having temporary " + "technical issues.\nIn this case, press enter and " + "try again later.\nOtherwise you can type 'reset' to " + "reset the current credentials and set them " + "again the next start.\n> ") + if choice.lower().strip() == "reset": + settings.token = None + settings.email = None + settings.password = None + settings.save_settings() except KeyboardInterrupt: loop.run_until_complete(bot.logout()) except: