Package: discord-gateway
Discord gatewaySans-I/O Python implementation of the Discord gateway.Sans-I/O means that this implements no I/O (network) and operates purely on the
bytes given usingwsproto.It means that this implementation can be reused for libraries implemented in a
threading fashion or asyncio/trio/curio.Reference ImplementationFor a reference implementation seewumpy-gateway.
It is designed to handle all sort of network failures and race conditions.QuickstartHere's a very minimal implementation using thesocketlibrary andthreadingfor the heartbeating (it does not handle reconnecting or any form of
unexpected disconnections):importsocketimportsslimportthreadingimporttimefromsysimportplatformimportcertififromdiscord_gatewayimportDiscordConnectionTOKEN='YOUR_VERY.WELL.HIDDEN_TOKEN'RECV_SIZE=65536SERVER_NAME='gateway.discord.gg'defheartbeat(conn,sock):whileTrue:sock.send(conn.heartbeat())time.sleep(conn.heartbeat_interval)defrecv_event(conn,sock):whileTrue:foreventinconn.events():returneventforto_sendinconn.receive(sock.recv(RECV_SIZE)):sock.send(to_send)defmain():# Setup the socket and SSL for the WebSocket Secure connection.conn=DiscordConnection(SERVER_NAME,encoding='json')ctx=ssl.create_default_context(cafile=certifi.where())sock=socket.create_connection(conn.destination)sock=ctx.wrap_socket(sock,server_hostname=SERVER_NAME)sock.send(conn.connect())# Convert to a WebSocket# Receive the very first HELLO event.hello=recv_event(conn,sock)# Send RESUME or IDENTIFY depending on state (will always be False# when initially connecting, but may be different when reconnecting).ifconn.should_resume:sock.send(conn.resume(TOKEN))else:sock.send(conn.identify(token=TOKEN,intents=65535,properties={'$os':platform,'$browser':'discord-gateway','$device':'discord-gateway'},))heartbeater=threading.Thread(target=heartbeat,args=(conn,sock))heartbeater.start()try:whileTrue:event=recv_event(conn,sock)print('Received:',event)finally:sock.shutdown(socket.SHUT_WR)sock.close()if__name__=='__main__':main()Comprehensive GuideConnectingThe very first thing required to use the Discord Gateway is to connect using a
TCP socket with TLS enabled.Start by creating an instance ofDiscordConnection, passing in the URI and
encoding to use (most likely'json'), assign it to a variable calledconn.Now, connect a TCP socket toconn.destination(this is a property that
returns a tuple with the host and port to use - as often used when connecting)
and wrap it with TLS. Using the built-insocketandsslmodule withcertifithat looks like this:SERVER_NAME='gateway.discord.gg'conn=DiscordConnection(SERVER_NAME,encoding='json')sock=socket.create_connection(conn.destination)ctx=ssl.create_default_context(cafile=certifi.where())sock=ctx.wrap_socket(sock,server_hostname=SERVER_NAME)After the connection is established, generate a HTTP 101 Switching Protocols
request by callingconn.connect()and sending it over the socket.Bootstrapping and HELLOsIt is now time to communicate over the WebSocket - or almost!You have yet to receive anything over the socket, not even the response to the
HTTP request made. Create a while-loop that will iterate until a full event
has been received, but it is important to wrap it in a try/except statement
that catches theConnectionRejectedin-case Discord rejects the WebSocket
upgrade.In each iteration the data from the socket should be passed toconn.receive(), which then returns a list of bytes that should be sent back
to the socket. The code should roughly look like this:hello=NonewhilehelloisNone:try:forto_sendinconn.receive(sock.recv(65535)):sock.send(to_send)exceptConnectionRejected:print('Discord rejected the connection!')raiseforhelloinconn.events():# After this has been executed, the hello variable will no longer# be set to None and the while-loop will exit.breakWhen this loop has exited all internal state of discord-gateway should be ready
to IDENTIFY or RESUME depending onconn.should_resume(which is always
falsely during startup - but useful during reconnections).HeartbeatingThe next part is maintaining the connection established, this is done by
launching a concurrent thread/task (depending on how concurrency is handled).The only purpose of this concurrent heartbeater is to periodically generate
a HEARTBEAT commands and sleep:defheartbeater(conn:DiscordConnection,sock:socket.socket)->typing.NoReturn:# Discord recommends sleeping this random amount of time before the first# heartbeat, this is to relieve Discord's servers when they are starting# up again after downtimes.time.sleep(random.random()*conn.heartbeat_interval)whileTrue:sock.send(conn.heartbeat())time.sleep(conn.heartbeat_interval)After this, you are now fully connected and can receive events similar to how
you received the HELLO event.DisconnectingExpected DisconnectionsWhen disconnecting you need to follow the WebSocket protocol, to start off
generate a closing frame usingconn.close()and send it over the socket.Afterwards, receive data until discord-gateway raisesCloseDiscordConnectionand send thedataattribute over the socket. Lastly shutdown the write end
of the socket usingsock.shutdown(socket.SHUT_WR)and then fully close the
socket. In code that would look like this:sock.send(conn.close())try:whileTrue:fordatainconn.receive(socket.recv(65535)):sock.send(data)exceptCloseDiscordConnectionaserr:iferr.dataisnotNone:sock.send(err.data)# Shutdown the socket now that the WebSocket is closedsock.shutdown(socket.SHUT_WR)received=Nonewhilereceived!=b'':received=sock.recv(65535)sock.close()Unexpected DisconnectionsDiscord-gateway automatically schedules for reconnections in cases such as
un-acknowledged heartbeats and RECONNECT events. That said, discord-gateway
still needs your help to actually reconnect the underlying socket.Like with expected disconnections discord-gateway raisesCloseDiscordConnectionwhen the closing handshake is being made, just follow
what was done for expected disconnects and then reconnect but do not create a
newDiscordConnectioninstance. Doing so looses information such as why the
reconnection is happening in the first place. Instead call thereconnect()method to reset the internal state.reconnect()returns an integer representing the amount of seconds to wait
before retrying, this will expotentially increase until a successful HELLO
event is received. To take advantage of this, you should structure a retry loop
like this:whileTrue:time.sleep(conn.reconnect())try:# Connect to the socket as normal, see "Bootstrapping and HELLOs" above...exceptEOFError:# Note that in some implementations an empty bytes object is returned# to signal this. It essentially means that the socket closed. Don't# forget to clean up the socket in all of these except-clauses....continueexceptConnectionRejectedaserr:...continueelse:# If the bootstrapping succeeded and we received a HELLO event, then# we can finally exit this loop.breakUse theshould_reconnect()function to determine whether to reconnect, if it
returnsFalsethat signals that the connectionmust notbe reconnected.
It is recommended to raise an error or similar in this case.Race conditionsBecause of the concurrent heartbeater there are potential race conditions,
primarily when reconnecting.The easiest way to fix this is to use a lock that is held when reconnecting,
the heartbeater has to acquire it everytime it tries to send a heartbeat.Another potential issue is sending a heartbeat while the closing handshake is
in progress - that is, the client has sent one closing frame but not yet
received it from the other end. Discord-gateway provides a simple property
calledclosed, if this is set just skip sending anything and sleep again.Following the theme ofsockets and threads here is an example of an improved
heartbeater:lock=...defheartbeater(conn,sock):# Discord recommends sleeping this random amount of time before the first# heartbeat, this is to relieve Discord's servers when they are starting# up again after downtimes.time.sleep(random.random()*conn.heartbeat_interval)whileTrue:withlock:ifnotconn.closing:sock.send(conn.heartbeat())time.sleep(conn.heartbeat_interval)