├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── commands ├── fun.py └── setup.py ├── ext ├── context.py └── table.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | .static_storage/ 57 | .media/ 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | .vscode 108 | secrets.txt -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to fourjr/discord-cli 2 | 3 | Pull requests are always cool! How'd you get started configuring your workspace and all though? Read on. 4 | 5 | ## Setting up the environment 6 | 7 | If you already have `discord-cli` working in a location, it should be good enough to edit the code from there and test it out! However, there are a few guidelines that comes when you want to open a pull request and get it merged: 8 | 9 | - The code must follow [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/) at all times. Docstrings for all functions/commands have to be present. 10 | - All new features must be **locally tested**. They should be tested with *bot accounts* as that is the main type of login that we are supporting. Testing it with *user accounts* are fine too, but not completely required. 11 | - All commands that send embeds must not send an embed if `bot.is_bot` is `False`. 12 | - Your attitude can't be shit, if you are asked to modify something or make something better, or the community tells you better ways to do x, you can't have the "it works" attitude ¯\\\_(ツ)_/¯ 13 | - Open PRs for a use. And that use can't be to get your name in that contributors list. 14 | - Use `cprint` and not `print` to include a spark of color! 15 | 16 | ## The PR itself 17 | 18 | Ensure the title helps other people understand what your edit does at first glance. It doesn't have to be descriptive though. `Adds a cat command`, that's fine :) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-cli 2 | A command line interface to use Discord. 3 | 4 | ## DISCLAIMER - PLEASE READ 5 | This is for the 99.7% who refuse to read Discord ToS (and possibly this disclaimer as well). 6 | 7 | This project wasn't meant to replace discord's official client in any way shape or form. 8 | If your account gets banned, don't come creating 700 Github Issues with you 2000 alts. I already warned you here. 9 | 10 | Using this program with a user account is much more dangerous than a bot account. I'm using `discord.py 1.0.0a` right here. It has `user-agent`s in it's requests to the Discord API to say that it isn't a client which means it isn't hard for Discord to realise that you aren't using the client. 11 | 12 | This program will also allow you to send embeds (in the future). Sending these will raise a high alert as user accounts technically in the official clients aren't allowed to do this. 13 | 14 | Using external clients were against Discord's wish and will from the start. Always was against the terms of service but there are still people who do this. 15 | 16 | If you have finished reading this, and still want to have some super cool command line discord, go ahead and read on! Else, you better close this tab and clear your history before you get bANNed! 17 | 18 | TL;DR Don't blame me if you get banned. 19 | 20 | ## Is this project meant for production? 21 | No. 22 | This project is **far from complete**. Refer to some of the below sections for further explanation. 23 | 24 | You can read through the code and maybe learn some new things though :) 25 | 26 | ## How do you use this? 27 | I assume you already have at least Python 3.5 installed. 28 | I also assume you have `git clone`d this. 29 | 30 | Install requirements from `requirements.txt` 31 | ``` 32 | python -m pip install -r requirements.txt 33 | ``` 34 | 35 | Run the file 36 | ``` 37 | python main.py -t= -c= 38 | ``` 39 | 40 | An example 41 | ``` 42 | python main.py -t=MjM4NDk0NzU2NTIxMzc3Nzky.CunGFQ.wUILz7z6HoJzVeq6pyHPmVgQgV4 -c=381870553235193857 43 | ``` 44 | 45 | You can run the CLI without providing a token or a channel ID. If you do not provide a token, you will be prompted to include your email/password. This method does not work all the time, especially if you have not logged into Discord for a long time. It is still highly recommended to provide a token. 2FA is supported in this case. Of course, you would have to include a token if you are going to run this on the bot account. 46 | 47 | If you do not provide a chnanel ID, the user will log in with no specified channel. You would have to then input a channel ID for the CLI to work. 48 | 49 | ![image](https://i.imgur.com/QvY5GIM.png) 50 | *An example of a functioning CLI* 51 | 52 | ![image](https://i.imgur.com/z0kPupy.png) 53 | *The CLI Error Handling* 54 | 55 | ## What else do I want to be here? 56 | - [ ] Commands! 57 | - [x] /lenny 58 | - [x] /shrug 59 | - [x] /channel 60 | - [ ] /embed 61 | 62 | - [x] Remove f-strings - py3.5 compaitibility 63 | 64 | ## How can you help? 65 | [CONTRIBUTING.md](CONTRIBUTING.md) 66 | -------------------------------------------------------------------------------- /commands/fun.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | 4 | class Fun(commands.Cog): 5 | '''The cog consists of fun and misc commands''' 6 | def __init__(self, bot): 7 | self.bot = bot 8 | 9 | shortcuts = { 10 | 'shrug': r'¯\\\_(ツ)\_/¯', 11 | 'tableflip': '(╯°□°)╯︵ ┻━┻', 12 | 'unflip': '┬─┬ ノ( ゜-゜ノ)', 13 | 'lenny': '( ͡° ͜ʖ ͡°)' 14 | } 15 | 16 | @commands.command(aliases=list(shortcuts)) 17 | async def shortcut(self, ctx): 18 | '''Command to have shortcuts such as, shrug and tableflip, sent''' 19 | if ctx.invoked_with == 'shortcut': 20 | return await ctx.send(ctx.message.content) 21 | await ctx.send(self.shortcuts[ctx.invoked_with]) 22 | 23 | 24 | def setup(bot): 25 | '''Adds the cog''' 26 | bot.add_cog(Fun(bot)) 27 | -------------------------------------------------------------------------------- /commands/setup.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from termcolor import cprint 4 | 5 | from ext.table import Table 6 | 7 | 8 | class GuildConverter(commands.IDConverter): 9 | '''Converts to a :class:`discord.Guild`. 10 | 11 | The lookup strategy is as follows (in order): 12 | 1. Lookup by ID. 13 | 2. Lookup by name. 14 | ''' 15 | 16 | async def convert(self, ctx, argument): 17 | bot = ctx.bot 18 | result = None 19 | 20 | match = self._get_id_match(argument) 21 | 22 | if match is None: 23 | def check(guild): 24 | return guild.name == argument 25 | result = discord.utils.find(check, bot.get_all_guilds()) 26 | else: 27 | guild_id = int(match.group(1)) 28 | result = bot.get_guild(guild_id) 29 | 30 | if not isinstance(result, discord.Guild): 31 | raise commands.errors.BadArgument('Channel "{}" not found.'.format(argument)) 32 | 33 | return result 34 | 35 | 36 | class Setup(commands.Cog): 37 | def __init__(self, bot): 38 | self.bot = bot 39 | self.text_channel_conv = commands.TextChannelConverter() 40 | 41 | @commands.command(aliases=['exit']) 42 | async def logout(self, ctx): 43 | '''Logs the bot out''' 44 | await self.bot.logout() 45 | 46 | @commands.command() 47 | async def channel(self, ctx, channel_=None, guild: GuildConverter=None): 48 | '''Changes channels''' 49 | self.bot.pause() 50 | Table(ctx, self.channel_callback, *[i.name for i in ctx.guild.text_channels if i.permissions_for(ctx.guild.me).read_messages]).start() 51 | 52 | def channel_callback(self, ctx, name): 53 | self.bot.channel = discord.utils.get(ctx.guild.text_channels, name=name) 54 | cprint('Text channel set: #{0.name} in {0.guild.name}'.format(self.bot.channel), 'green') 55 | 56 | @commands.command(name='help') 57 | async def help_(self, ctx): 58 | '''Shows this message''' 59 | cprint('\n'.join(i.name + ' - ' + i.short_doc for i in self.bot.commands), 'cyan') 60 | 61 | 62 | def setup(bot): 63 | bot.add_cog(Setup(bot)) 64 | -------------------------------------------------------------------------------- /ext/context.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from discord.ext import commands 3 | from termcolor import cprint 4 | 5 | class ConsoleMessage: 6 | ''' 7 | ConsoleMessages are messages that do not appear in discord but have 8 | been sent through user_input and are being parsed 9 | 10 | This class only has 3 attributes and 0 methods: 11 | > content - Message Content 12 | > channel - Channel which the CLI is connected to 13 | > guild - Guild that the CLI is connected to (if applicable) 14 | ''' 15 | 16 | def __init__(self, content, bot): 17 | self.content = content.strip() 18 | self.channel = bot.channel 19 | self.guild = getattr(bot.channel, 'guild', False) or None 20 | 21 | class Context(commands.Context): 22 | '''A CustomContext for commands parsed via user_input''' 23 | 24 | def __init__(self, **attrs): 25 | self.bot = attrs.pop('bot', None) 26 | self.view = attrs.pop('view', None) 27 | self.command = attrs.pop('command', None) 28 | self.message = ConsoleMessage(attrs.pop('message', None), self.bot) 29 | 30 | self.channel = self.bot.channel 31 | self.guild = getattr(self.bot.channel, 'guild', False) or None 32 | 33 | self.check_channel() 34 | 35 | def check_channel(self): 36 | '''Checks if channel is defined and gives error messages''' 37 | if self.channel is None: 38 | try: 39 | self.bot.channel = self.bot.get_channel(int(self.message.content)) 40 | except ValueError: 41 | cprint('Channel not set. Send a channel ID to start the program', 'red') 42 | else: 43 | if self.bot.channel is None: 44 | cprint('Invalid text channel.', 'red') 45 | else: 46 | if hasattr(self.bot.channel, "name"): 47 | cprint('Text channel set: #{}'.format(self.bot.channel.name), 'green') 48 | else: 49 | cprint('Text channel set: #{}'.format(self.bot.channel), 'green') 50 | 51 | async def send(self, *args, **kwargs): 52 | '''Replaces Messageable.send because self._state does not exist''' 53 | await self.channel.send(*args, **kwargs) 54 | -------------------------------------------------------------------------------- /ext/table.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | import keyboard 5 | from termcolor import cprint 6 | 7 | class Table: 8 | def __init__(self, ctx, callback, *items, **options): 9 | self.ctx = ctx 10 | self.callback = callback 11 | self.items = list(items) 12 | self.options = { 13 | 'padding': options.get('padding') or ' ', 14 | 'max_num': 3 15 | } 16 | self.current_index = 0 17 | self.rows = 0 18 | self.maxlen = max([len(i) for i in items]) 19 | self.full_len = self.maxlen + len(self.options['padding']) 20 | 21 | for n, i in enumerate(items): 22 | self.items[n] = i + ' ' * (self.maxlen - len(i)) + self.options['padding'] 23 | self.items[n] = ' ' + self.items[n] 24 | if (n + 1) % self.options['max_num'] == 0 or n == len(items) - 1: 25 | self.items[n] += '\n' 26 | self.rows += 1 27 | 28 | def start(self): 29 | for n, i in enumerate(self.items): 30 | if n == 0: 31 | cprint(i, 'grey', 'on_white', end='') 32 | else: 33 | cprint(i, end='') 34 | self.move_cursor(x=-1, y=self.rows) 35 | self.wait_for_input() 36 | 37 | def move_cursor(self, **coords: int): 38 | y = coords.get('y') 39 | x = coords.get('x', '') 40 | if y: 41 | if abs(y) == y: 42 | y = '\033[{}A'.format(y) 43 | else: 44 | y = '\033[{}B'.format(abs(y)) 45 | sys.stdout.write(y) 46 | if x: 47 | if abs(x) == x: 48 | x = '\033[{}C'.format(x) 49 | else: 50 | x = '\033[{}D'.format(abs(x)) 51 | sys.stdout.write(x) 52 | sys.stdout.flush() 53 | 54 | def reload_text(self, index): 55 | if index == self.current_index: 56 | cprint(self.items[index], 'grey', 'on_white', end='') 57 | else: 58 | cprint(self.items[index], end='') 59 | 60 | def wait_for_input(self): 61 | time.sleep(1) 62 | keyboard.on_release_key('up', self.input_callback) 63 | keyboard.on_release_key('down', self.input_callback) 64 | keyboard.on_release_key('left', self.input_callback) 65 | keyboard.on_release_key('right', self.input_callback) 66 | keyboard.on_release_key('enter', self.input_callback) 67 | 68 | def input_callback(self, key): 69 | if key.name == 'right': 70 | self.current_index += 1 71 | self.reload_text(self.current_index - 1) 72 | self.reload_text(self.current_index) 73 | if (self.current_index + 1) % self.options['max_num'] == 0: 74 | self.move_cursor(x=self.full_len * (self.options['max_num'] - 1) + 2, y=1) 75 | else: 76 | self.move_cursor(x=-self.full_len - 1) 77 | 78 | if key.name == 'left': 79 | self.current_index -= 1 80 | self.reload_text(self.current_index + 1) 81 | if (self.current_index + 2) % self.options['max_num'] == 0: 82 | self.move_cursor(x=self.full_len * (self.options['max_num'] - 2) + 1, y=1) 83 | elif (self.current_index + 1) % self.options['max_num'] == 0: 84 | self.move_cursor(x=self.full_len + 1, y=1) 85 | else: 86 | self.move_cursor(x=-self.full_len * (self.options['max_num'] - 1) - 2) 87 | self.reload_text(self.current_index) 88 | if (self.current_index + 1) % self.options['max_num'] == 0: 89 | self.move_cursor(x=self.full_len * (self.options['max_num'] - 1) + 2, y=1) 90 | else: 91 | self.move_cursor(x=-self.full_len - 1) 92 | 93 | if key.name == 'enter': 94 | current_column = ((self.current_index + 1) % self.options['max_num']) 95 | current_row = ((self.current_index + 1) // self.options['max_num']) + 1 96 | self.move_cursor(x=-self.full_len * (current_column - 1) - 1, y=self.rows % current_row) 97 | for _ in range(self.rows): 98 | sys.stdout.write('\033[K') 99 | self.move_cursor(y=-1) 100 | self.move_cursor(x=-self.full_len * self.rows, y=self.rows) 101 | keyboard.unhook_all() 102 | 103 | self.ctx.bot.paused = False 104 | try: 105 | import msvcrt 106 | while msvcrt.kbhit(): 107 | msvcrt.getch() 108 | except ImportError: 109 | import termios #for linux/unix 110 | termios.tcflush(sys.stdin, termios.TCIOFLUSH) 111 | self.callback(self.ctx, self.items[self.current_index].strip()) 112 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import json 3 | import os 4 | import platform 5 | import re 6 | from argparse import ArgumentParser 7 | from getpass import getpass 8 | 9 | import colorama 10 | import discord 11 | import requests 12 | from aioconsole.stream import ainput 13 | from discord.ext import commands 14 | from termcolor import cprint 15 | 16 | from ext.context import Context 17 | 18 | parser = ArgumentParser(description='Runs a Discord Account in the CLI.', usage='main.py token [-c CHANNEL] [-h]') 19 | parser.add_argument('-t', '--token', help='Your discord account/bot token') 20 | parser.add_argument('-c', '--channel', help='A single default channel you want your account to run in', type=int) 21 | args = parser.parse_args() 22 | 23 | colorama.init() 24 | 25 | # CHECKS TO ENSURE YOU DON'T TRY TO LOAD UP WITH UNSUPPORTED # 26 | if float('.'.join(platform.python_version().split('.')[:2])) < 3.5: 27 | cprint('\n'.join(( 28 | 'You are using an unsupported version of Python.', 29 | 'Please upgrade to at least Python 3.5 to use discord-cli', 30 | 'You are currently on ' + platform.python_version() 31 | )), 'red') 32 | exit(0) 33 | 34 | # PROGRAM # 35 | 36 | 37 | class Bot(commands.Bot): 38 | '''Bot subclass to handle CLI IO''' 39 | def __init__(self): 40 | super().__init__(command_prefix='/') 41 | self.session = aiohttp.ClientSession(loop=self.loop) 42 | self.loop.create_task(self.user_input()) 43 | self.channel = None 44 | self.is_bot = None 45 | self.paused = False 46 | self.role_converter = commands.RoleConverter() 47 | self.member_converter = commands.MemberConverter() 48 | self.remove_command('help') 49 | 50 | for i in [i.replace('.py', '') for i in os.listdir('commands') if i.endswith('.py')]: 51 | self.load_extension('commands.' + i) 52 | 53 | cprint('Logging in...', 'green') 54 | self.run() 55 | 56 | def pause(self): 57 | self.paused = True 58 | 59 | async def on_connect(self): 60 | '''Sets the client presence''' 61 | self.is_bot = self._connection.is_bot 62 | await self.change_presence(status=discord.Status.offline, afk=True) 63 | 64 | async def on_ready(self): 65 | '''Sets up the channel''' 66 | self.channel = self.get_channel(args.channel) 67 | 68 | if self.channel is None: 69 | if args.channel is not None: 70 | cprint('Invalid channel ID provided.', 'red') 71 | 72 | cprint('\n'.join(('Logged in as {0.user} in no specified channel.'.format(self), 73 | 'Send a channel ID to start the program')), 'green') 74 | else: 75 | cprint('Logged in as {0.user} in #{0.channel}'.format(self), 'green') 76 | 77 | async def on_message(self, message): 78 | '''Prints to console upon new message''' 79 | await self.wait_until_ready() 80 | if not self.channel or self.paused: 81 | return 82 | if message.channel.id == self.channel.id: 83 | if message.author.id == self.user.id: 84 | color = 'cyan' 85 | else: 86 | color = 'yellow' 87 | 88 | match = [i.group(0) for i in re.finditer(r'<(@(!?|&?)|#)([0-9]+)>', message.content)] 89 | if match: 90 | for mention in match: 91 | mention_id = int( 92 | mention 93 | .replace('<@', '') 94 | .replace('>', '') 95 | .replace('!', '') 96 | .replace('&', '') 97 | .replace('<#', '') 98 | ) 99 | 100 | def check(role): 101 | return role.id == mention_id 102 | result = self.get_user(mention_id) or discord.utils.find(check, message.guild.roles) or self.get_channel(mention_id) 103 | message.content = message.content.replace(mention, '@{}'.format(result)) 104 | 105 | cprint('{0.author}: {0.content}'.format(message), color) 106 | 107 | async def user_input(self): 108 | '''Captures user input as a background task asynchronusly''' 109 | await self.wait_until_ready() 110 | while not self.is_closed(): 111 | if self.paused: 112 | continue 113 | try: 114 | text = await ainput() 115 | except EOFError: 116 | continue 117 | if text: 118 | ctx = await self.get_context(text) 119 | 120 | # MENTION CONVERT # 121 | match = [i.group(1).strip() for i in re.finditer(r'@([^ @]+)', text)] 122 | if match: 123 | for mention in match: 124 | try: 125 | result = await self.member_converter.convert(ctx, mention) 126 | except commands.errors.BadArgument: 127 | try: 128 | result = await self.role_converter.convert(ctx, mention) 129 | except commands.errors.BadArgument: 130 | result = None 131 | 132 | if result is not None: 133 | text = text.replace('@' + mention, result.mention) 134 | 135 | # END OF MENTION CONVERt # 136 | 137 | if ctx.channel: 138 | try: 139 | if ctx.command is None: 140 | await self.channel.send(text) 141 | else: 142 | await ctx.command.invoke(ctx) 143 | 144 | except discord.DiscordException as error: 145 | cprint(error, 'red') 146 | 147 | async def get_context(self, message): 148 | '''Overwrites the default get_context''' 149 | 150 | view = commands.view.StringView(message) 151 | ctx = Context(view=view, bot=self, message=message) 152 | 153 | prefix = await self.get_prefix(message) 154 | invoked_prefix = prefix 155 | 156 | if isinstance(prefix, str): 157 | if not view.skip_string(prefix): 158 | return ctx 159 | else: 160 | invoked_prefix = discord.utils.find(view.skip_string, prefix) 161 | if invoked_prefix is None: 162 | return ctx 163 | 164 | invoker = view.get_word() 165 | ctx.invoked_with = invoker 166 | ctx.prefix = invoked_prefix 167 | ctx.command = self.all_commands.get(invoker) 168 | return ctx 169 | 170 | def get_all_guilds(self): 171 | for guild in self.guilds: 172 | yield guild 173 | 174 | def run(self): 175 | '''Starts the bot''' 176 | if not getattr(args, 'token'): 177 | 178 | email = input('Enter your email: ') 179 | password = getpass('Enter your password: ') 180 | payload = { 181 | 'email': email, 182 | 'password': password, 183 | 'captcha_key': None, 184 | 'undelete': False 185 | } 186 | endpoint = 'https://discordapp.com/api/v6/auth/login' 187 | # inspect.currentframe().f_back.f_code.co_name 188 | with requests.post(endpoint, json=payload) as resp: 189 | data = json.loads(resp.text) 190 | if resp.status_code == 400: 191 | if data == {'password': ['Password does not match.']}: 192 | cprint('Invalid credentials provided.', 'red') 193 | elif data == {'email': ['Not a well formed email address.']}: 194 | cprint('Not a well formed email address.', 'red') 195 | elif data == {'captcha_key': ['captcha-required']}: 196 | cprint(''.join(('Due to certain limitations, in order to use this CLI with email/password. ', 197 | 'You would have to either:\n-Activate 2FA,', 198 | '\n-Use a token to login, \n-Login on the actual discord recently')), 'red') 199 | else: 200 | cprint('Something else went wrong. Could be invalid email.', 'red') 201 | return 202 | 203 | if data.get('mfa'): 204 | cprint('2FA Required', 'cyan') 205 | payload = { 206 | 'ticket': data['ticket'] 207 | } 208 | if data.get('sms'): 209 | cprint('\n'.join(('If you want your 2FA Code to be sent via SMS, input "SMS" without the quotes.', 210 | 'Else, input your 2FA Code.')), 'cyan' 211 | ) 212 | 213 | sms = input('>') 214 | 215 | if sms.upper() == 'SMS': 216 | endpoint = 'https://canary.discordapp.com/api/v6/auth/mfa/sms/send' 217 | auth_code = json.loads(requests.post(endpoint, json=payload).text) 218 | cprint('Code has been sent to {}. Please enter your code below\n>'.format(auth_code['phone']), 'cyan') 219 | payload['code'] = input('>') 220 | 221 | endpoint = 'https://canary.discordapp.com/api/v6/auth/mfa/sms' 222 | auth = requests.post(endpoint, json=payload) 223 | 224 | elif sms in ('', '\n'): 225 | cprint('Invalid 2FA Code', 'red') 226 | 227 | else: 228 | payload['code'] = sms 229 | endpoint = 'https://canary.discordapp.com/api/v6/auth/mfa/totp' 230 | auth = requests.post(endpoint, json=payload) 231 | else: 232 | payload['code'] = sms 233 | endpoint = 'https://canary.discordapp.com/api/v6/auth/mfa/totp' 234 | auth = requests.post(endpoint, json=payload) 235 | 236 | auth_data = json.loads(auth.text) 237 | if auth_data == {'code': 60008, 'message': 'Invalid two-factor code'}: 238 | cprint('Invalid 2FA Code', 'red') 239 | return 240 | 241 | data['token'] = auth_data['token'] 242 | 243 | super().run(data['token'], bot=False) 244 | else: 245 | try: 246 | self.loop.run_until_complete(self.start(args.token)) 247 | except discord.errors.LoginFailure: 248 | try: 249 | super().run(args.token, bot=False) 250 | except discord.errors.LoginFailure: 251 | cprint('Invalid token provided.', 'red') 252 | finally: 253 | self.loop.close() 254 | 255 | 256 | if __name__ == '__main__': 257 | Bot() 258 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor 2 | colorama 3 | discord.py 4 | aioconsole 5 | keyboard --------------------------------------------------------------------------------