├── LICENSE ├── README.md └── ripcord.py /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ripcord-api 2 | 3 | This is old, unnecessary, and probably out of date. Do not use it, except perhaps as a reference for your own work. 4 | 5 | An unofficial list of discord API libraries can be found [here](https://discordapi.com/unofficial/libs.html). The majority of these libraries can be used to create Discord self-bots1 (or can be easily modified to do so2), and eliminates the need for this library. 6 | 7 | Below is the original README.md. 8 | 9 | [1] *self-bot* meaning a programmable Discord bot masquerading as a real Discord user. 10 | [2] I did it with JDA. 11 | 12 | ----- 13 | 14 | This project endeavors to reverse engineer as much of the unreleased Discord Client API as possible, and produce a library of functions that lets programmers: 15 | 16 | - Write a better, more fully featured, and open source Discord client. 17 | - Write bots which can respond to voice commands. 18 | - Implement optional E2EE in a Discord Client. 19 | 20 | Also because message deletion is stupid. 21 | 22 | [Proof of concept](https://www.youtube.com/watch?v=bQk-ZJPecSc) - Part of this project was ported to Java to write a small standalone client, just to show that it was possible. 23 | 24 | ## Currently implemented/figured out functions 25 | 26 | These are functions of `DiscordClient`. 27 | 28 | `login(email, password)` 29 | Sends a login request with email:password, and gets an authtoken. 30 | 31 | `logout()` 32 | Sends a logout request. Closes websocket connection. 33 | 34 | `get_me()` 35 | Gets information about the logged in user, saving the following information: username, email, phone number, avatar, id, discriminator, verified email. 36 | 37 | `retrieve_websocket_gateway()` 38 | Gets the websocket URL. Usually `wss://gateway.discordapp.com/`. 39 | 40 | `download_messages(channelid, limit=50)` 41 | Returns up to 50 (unless specified otherwise) messages from the channel `channelid`. 42 | 43 | `connect_websocket(gateway_url)` 44 | Connects to and uses the websocket at `gateway_url`. 45 | 46 | `send_message(channelid, message, tts=False, nonce="123")` 47 | Sends message with content `message` to the channel given by `channelid`. Optional flag to use Discord's TTS feature. 48 | 49 | `send_start_typing(channelid)` 50 | Sends a signal to show that the user is typing in the channel given by `channelid`. 51 | 52 | `send_presence_change(presence)` 53 | Changes the status of the user. `presence` can be one of `idle`, `online`, `dnd`, `invisible`. 54 | 55 | `send_view_server(serverid)` 56 | Sends a (OP 12) signal declaring which server you are looking at. You will start receiving TYPING_START packets from users in these servers. 57 | 58 | `retrieve_servers()` 59 | Retrieves a list of all the servers the current user is a member of. 60 | 61 | `retrieve_server_channels(serverid)` 62 | Retrieves a list of channels in the server given by `serverid`. This will return ALL channels that exist, including voice channels, and channels that you are not a member of. 63 | 64 | `retrieve_server_members(serverid)` 65 | Retrieves a list of members in the server given by `serverid`. Members who have a special nickname in `serverid` will have a 'nick' field. 66 | 67 | 68 | The working project name is `Communication Security is Mandatory`. 69 | -------------------------------------------------------------------------------- /ripcord.py: -------------------------------------------------------------------------------- 1 | from websocket import create_connection 2 | 3 | import requests, json, threading, select, multiprocessing, time, datetime 4 | 5 | 6 | class DiscordClient: 7 | def __init__(self): 8 | self.login_url = 'https://discordapp.com/api/v6/auth/login' 9 | self.me_url = 'https://discordapp.com/api/v6/users/@me' 10 | self.settings_url = 'https://discordapp.com/api/v6/users/@me/settings' 11 | self.guilds_url = 'https://discordapp.com/api/v6/users/@me/guilds' 12 | self.gateway_url = 'https://discordapp.com/api/v6/gateway' 13 | self.logout_url = 'https://discordapp.com/api/v6/auth/logout' 14 | self.track_url = 'https://discordapp.com/api/v6/track' 15 | self.members_url = 'https://discordapp.com/api/v6/guilds/{}/members' 16 | 17 | self.ws_gateway_query_params = '/?encoding=json&v=6' 18 | 19 | self.headers = {'User-Agent':'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0'} 20 | 21 | self.ws = None 22 | self.ws_send_queue = multiprocessing.Queue() 23 | 24 | self.message_counter = 0 25 | 26 | self.requester = requests.Session() 27 | 28 | self.servers_viewing = [] 29 | 30 | self.print_traffic = False 31 | 32 | def do_request(self, method : str, url : str, data=None, headers={}, params=None): 33 | resp = self.requester.request(method, url, data=data, headers={**self.headers, **headers}, params=params) 34 | if self.print_traffic: print('%s %s with data %s -- %i\n' % (method, url, data, resp.status_code)) 35 | return resp 36 | 37 | def login(self, email : str, password : str): 38 | """ Attempts to login with the given credentials. 39 | Returns true on sucess, and stores the authtoken in self.token 40 | 41 | Returns: 42 | bool: True if successful 43 | """ 44 | 45 | data = json.dumps({'email': email, 'password':password}).encode('utf-8') 46 | 47 | print('Attempting login of', email, ':', '*'*len(password)) 48 | req = self.do_request('POST', self.login_url, data=data, headers={'Content-Type':'application/json'}) 49 | self.debug = req 50 | 51 | if req.status_code == 200: 52 | self.token = req.json()['token'] 53 | return True 54 | return False 55 | 56 | def logout(self): 57 | """ Attempts to logout. 58 | Will close the websocket client if opened. 59 | 60 | Returns: 61 | bool: True if successful 62 | """ 63 | 64 | data = json.dumps( 65 | { 'provider':None, 'token':None } 66 | ) 67 | 68 | if self.ws and self.ws.connected: 69 | self.ws.close() 70 | self.ws_send_queue.put('nosend') 71 | 72 | req = self.do_request('POST', self.logout_url, headers={'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 73 | 74 | self.debug = req.text 75 | 76 | if req.status_code == 204: 77 | return True 78 | else: 79 | return False 80 | 81 | 82 | def get_me(self): 83 | """ Downloads information about the client. 84 | Stores this information in a dict in self.me. 85 | 86 | Returns: 87 | bool: True if successful 88 | """ 89 | 90 | # req = requests.get(self.me_url, headers={**self.headers, 'Authorization':self.token}) 91 | req = self.do_request('GET', self.me_url, headers={'Authorization':self.token}) 92 | if req.status_code == 200: 93 | data = req.json() 94 | 95 | self.me = data 96 | 97 | return True 98 | return False 99 | 100 | def retrieve_websocket_gateway(self): 101 | """ Attempts to get the websocket URL. 102 | 103 | Returns: 104 | str: The gateway URL. 105 | """ 106 | # req = requests.get(self.gateway_url, headers={**self.headers, 'Authorization':self.token}) 107 | req = self.do_request('GET', self.gateway_url, headers={'Authorization':self.token}) 108 | 109 | if req.status_code == 200: 110 | data = req.json() 111 | return data['url'] 112 | return False 113 | 114 | def download_messages(self, channelid : str, limit=50): 115 | """ Downloads messages for a specific channel id. Must have an authtoken. 116 | 117 | Returns: 118 | list: A list of the most recent messages. 119 | """ 120 | request_url = 'https://discordapp.com/api/v6/channels/{}/messages?limit={}'.format(channelid, limit) 121 | # req = requests.get(request_url, headers={**self.headers, 'Authorization':self.token}) 122 | req = self.do_request('GET', request_url, headers={'Authorization':self.token}) 123 | if req.status_code == 200: 124 | data = req.json() 125 | return data 126 | return req 127 | 128 | def connect_websocket(self, gateway_url : str): 129 | # do connect to websocket url gateway_url 130 | self.ws = create_connection(gateway_url + self.ws_gateway_query_params) 131 | 132 | self.ws_thread = threading.Thread(target=self.websocket_loop) 133 | self.ws_ping_thread = threading.Thread(target=self.websocket_ping) 134 | self.ws_thread.start() 135 | print('Started websocket thread\n') 136 | 137 | def websocket_loop(self): 138 | while self.ws.connected: 139 | readable, writable, executable = select.select( [self.ws, self.ws_send_queue._reader], [], [], 1.0 ) 140 | 141 | if not self.ws.connected: 142 | break 143 | 144 | for item in readable: 145 | if item == self.ws: 146 | read = self.ws.recv() 147 | 148 | if self.print_traffic: 149 | try: 150 | print('RECEIVED %s\n' % json.dumps(json.loads(read), indent=4, separators=(',', ': '))) 151 | except: 152 | print('RECEIVED %s\n' % read) 153 | 154 | self.message_counter += 1 155 | if type(read) == str and len(read) >= 2: # server information packet 156 | data = json.loads(read) 157 | if data['op'] == 10: 158 | self.heartbeat_interval = data['d']['heartbeat_interval'] / 1000 159 | self.ws_ping_thread.start() 160 | print('Started ping thread') 161 | 162 | elif data['op'] == 11: # Ping packet -- do not count pings! 163 | client.message_counter -= 1 164 | 165 | else: 166 | self.ws_recv_callback(data) 167 | 168 | elif item == self.ws_send_queue._reader: 169 | while not self.ws_send_queue.empty(): 170 | read = self.ws_send_queue.get() 171 | 172 | if self.print_traffic: 173 | try: 174 | print('SENDING %s\n' % json.dumps(json.loads(read), indent=4, separators=(',', ': '))) 175 | except: 176 | print('SENDING %s\n' % read) 177 | 178 | self.ws.send( read ) 179 | 180 | print('Websocket loop thread exited.') 181 | 182 | def websocket_ping(self): 183 | ticker = 0 184 | delta = self.heartbeat_interval / 60.0 185 | while self.ws.connected: 186 | time.sleep(delta) 187 | 188 | if not self.ws.connected: 189 | break 190 | 191 | if delta > self.heartbeat_interval: 192 | ticker = 0 193 | 194 | ping_packet = json.dumps( {'op':1, 'd':self.message_counter} ) 195 | self.websocket_send( ping_packet ) 196 | print('Sent ping.') 197 | 198 | print('Ping thread exited.') 199 | 200 | 201 | def websocket_send(self, data : bytes): 202 | self.ws_send_queue.put(data) 203 | 204 | def websocket_received_callback(self, callback): 205 | # call the callback function when receiving a packet 206 | self.ws_recv_callback = callback 207 | 208 | def send_message(self, channelid : str, message : str, tts=False, nonce="123"): 209 | """ Sends a message to a specific channel. 210 | 211 | Returns: 212 | bool: True if successful 213 | """ 214 | 215 | data = json.dumps( 216 | {"content": message, "tts":tts, "nonce": nonce} 217 | ) 218 | 219 | request_url = 'https://discordapp.com/api/v6/channels/{}/messages'.format(channelid) 220 | # req = requests.post(request_url, headers={**self.headers, 'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 221 | req = self.do_request('POST', request_url, headers={'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 222 | if req.status_code == 200: 223 | self.debug = req.json() 224 | 225 | return True 226 | 227 | self.debug = req.text 228 | return False 229 | 230 | def send_start_typing(self, channelid : str): 231 | """ Sends a signal to start typing to a specific channel. 232 | 233 | Returns: 234 | bool: True if successful 235 | """ 236 | 237 | request_url = 'https://discordapp.com/api/v6/channels/{}/typing'.format(channelid) 238 | # req = requests.post(request_url, headers={**self.headers, 'Authorization':self.token}) 239 | req = self.do_request('POST', request_url, headers={'Authorization':self.token}) 240 | if req.status_code == 204: 241 | return True 242 | self.debug = req 243 | return False 244 | 245 | def send_presence_change(self, presence : str): 246 | """ Sends a presence update. 247 | presence should be one of 'idle', 'online', 'dnd', 'invisible' 248 | 249 | Returns: 250 | bool: True if successful 251 | """ 252 | 253 | data = json.dumps({ 254 | 'op': 3, 255 | 'd': { 256 | 'status': presence, 257 | 'since': 0, 258 | 'game': None, 259 | 'afk': False 260 | } 261 | }) 262 | 263 | self.websocket_send(data) 264 | 265 | data = json.dumps( 266 | {'status': presence} 267 | ) 268 | 269 | #req = requests.patch(request_url, headers={**self.headers, 'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 270 | req = self.do_request('PATCH', self.settings_url, headers={'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 271 | 272 | self.debug = req 273 | 274 | if req.status_code == 200: 275 | return True 276 | else: 277 | return False 278 | 279 | def send_game_change(self, gamename : str): 280 | """ Send a game change update. 281 | 282 | Returns: 283 | bool: True if successful 284 | """ 285 | 286 | # TODO FIX ME -- I DON'T WORK 287 | 288 | data = json.dumps( 289 | { 'event':'Launch Game', 'properties':{'Game': gamename}, 'token':self.token } 290 | ) 291 | 292 | # req = requests.post(self.track_url, headers={**self.headers, 'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 293 | req = self.do_request('POST', self.track_url, headers={**self.headers, 'Authorization':self.token, 'Content-Type':'application/json'}, data=data) 294 | 295 | self.debug = req 296 | 297 | if req.status_code == 204: 298 | return True 299 | else: 300 | return False 301 | 302 | def send_view_server(self, serverid : str): 303 | """ Send a server-viewing update (OP 12) packet. 304 | By sending this, the client will receive TYPING_START packets from the users in the channel. 305 | Other effects are not known. 306 | """ 307 | 308 | if not serverid in self.servers_viewing: 309 | self.servers_viewing.append(serverid) 310 | 311 | data = json.dumps({'op': 12, 'd':self.servers_viewing}) 312 | self.websocket_send(data) 313 | 314 | def retrieve_servers(self): 315 | """ Retrieve a list of servers user is connected to. 316 | 317 | Returns: 318 | list: List of servers. 319 | """ 320 | req = self.do_request('GET', self.guilds_url, headers={**self.headers, 'Authorization':self.token}) 321 | self.debug = req 322 | 323 | if req.status_code == 200: 324 | return req.json() 325 | 326 | return None 327 | 328 | def retrieve_server_channels(self, serverid : str): 329 | """ Retrieve a list of channels in the server. 330 | 331 | This list includes channels that the user is not a part of. 332 | Attempting to run download_messages on these channels yields a 403 Forbidden error. 333 | 334 | Returns: 335 | list: List of channels or None if request fails. 336 | """ 337 | req = self.do_request('GET', 'https://discordapp.com/api/v6/guilds/{}/channels'.format(serverid), headers={**self.headers, 'Authorization':self.token}) 338 | self.debug = req 339 | 340 | if req.status_code == 200: 341 | return req.json() 342 | 343 | return None 344 | 345 | def retrieve_server_members(self, serverid : str, limit=1000): 346 | """ Retrieves a list of members in the server given by serverid. 347 | 348 | Returns: 349 | list: List of members, up to limit. 350 | """ 351 | query_params = {'limit': limit} 352 | req = self.do_request('GET', self.members_url.format(serverid), headers={**self.headers, 'Authorization':self.token}, params=query_params) 353 | self.debug = req 354 | 355 | if req.status_code == 200: 356 | return req.json() 357 | 358 | return None 359 | 360 | 361 | if __name__ == '__main__': 362 | import sys 363 | 364 | client = DiscordClient() 365 | 366 | with open('credentials','r') as f: 367 | email,password = json.load(f) 368 | 369 | # do login 370 | if client.login( email, password ): 371 | print('Login successful - Authtoken: %s' % client.token) 372 | else: 373 | print('Failed to login.') 374 | sys.exit(0) 375 | 376 | # download @me data 377 | client.get_me() 378 | print(client.me) 379 | 380 | # get the gateway 381 | websocket_url = client.retrieve_websocket_gateway() 382 | print(websocket_url) 383 | 384 | # create websocket callback 385 | def ws_callback(message): 386 | if message['op'] == 0 and message['t'] == 'MESSAGE_CREATE': 387 | print('<%s> %s' % (message['d']['author']['username'], message['d']['content']) ) 388 | 389 | client.websocket_received_callback(ws_callback) 390 | 391 | # start the websocket 392 | client.connect_websocket(websocket_url) 393 | client.websocket_send(json.dumps({ 394 | "op": 2, 395 | "d": { 396 | "token": client.token, 397 | "properties": { 398 | "os": "Linux", 399 | "browser": "Firefox", 400 | "device": "", 401 | "referrer": "", 402 | "referring_domain": "" 403 | }, 404 | "large_threshold": 100, 405 | "synced_guilds": [], 406 | "presence": { 407 | "status": "online", 408 | "since": 0, 409 | "afk": False, 410 | "game": None 411 | }, 412 | "compress": True 413 | } 414 | })) 415 | 416 | client.websocket_send(json.dumps( 417 | {"op":4,"d":{"guild_id":None,"channel_id":None,"self_mute":True,"self_deaf":False,"self_video":False}} 418 | )) 419 | 420 | # download servers 421 | 422 | servers = client.retrieve_servers() 423 | 424 | # print channels 425 | for server in servers: 426 | serverid = server['id'] 427 | server_members = client.retrieve_server_members(serverid) 428 | 429 | print('Server:', server['name'], 'ID:', serverid) 430 | 431 | channels = client.retrieve_server_channels(serverid) 432 | 433 | print('Found %i channels:' % len(channels)) 434 | 435 | for chan in channels: 436 | if chan['type'] == 0: # regular channel 437 | print( '\t(%s) %s: %s' % (chan['id'], chan['name'], chan['topic']) ) 438 | 439 | elif chan['type'] == 2: 440 | print( '\t(%s) %s %ik [Voice Channel]' % (chan['id'], chan['name'], int(chan['bitrate']/1000)) ) 441 | 442 | print('\nThis server has %i members:' % len(server_members)) 443 | for member in server_members: 444 | print('\t%s: (%s) %s' % (member['user']['id'], member['user']['username'], member['nick'] if 'nick' in member else member['user']['username']) ) 445 | 446 | print('') 447 | 448 | ''' 449 | # Tests sending of start-typing and message requests 450 | client.send_start_typing('304959901376053248') 451 | time.sleep(1) 452 | client.send_message('304959901376053248', time.ctime()) 453 | ''' 454 | 455 | 456 | # Tests the presence update by cycling through all of them 457 | time.sleep(2) 458 | print(client.send_presence_change('idle')) 459 | time.sleep(2) 460 | print(client.send_presence_change('dnd')) 461 | time.sleep(2) 462 | print(client.send_presence_change('invisible')) 463 | time.sleep(2) 464 | print(client.send_presence_change('online')) 465 | 466 | # Tests server-viewing packets -- client should receive TYPING_START packets from the server given, after sending this packet 467 | print('sending OP 12') 468 | client.print_traffic = True 469 | client.send_view_server('181226314810916865') 470 | 471 | time.sleep(12) 472 | 473 | print('Signing out...') 474 | client.logout() 475 | --------------------------------------------------------------------------------