Search code examples
pythonpython-3.xdiscorddiscord.pypycord

How do I get mobile status for discord bot by directly modifying IDENTIFY packet?


Apparently, discord bots can have mobile status as opposed to the desktop (online) status that one gets by default.

bot having mobile status

After a bit of digging I found out that such a status is achieved by modifying the IDENTIFY packet in discord.gateway.DiscordWebSocket.identify modifying the value of $browser to Discord Android or Discord iOS should theoretically get us the mobile status.

After modifying code snippets I found online which does this, I end up with this :

def get_mobile():
    """
    The Gateway's IDENTIFY packet contains a properties field, containing $os, $browser and $device fields.
    Discord uses that information to know when your phone client and only your phone client has connected to Discord,
    from there they send the extended presence object.
    The exact field that is checked is the $browser field. If it's set to Discord Android on desktop,
    the mobile indicator is is triggered by the desktop client. If it's set to Discord Client on mobile,
    the mobile indicator is not triggered by the mobile client.
    The specific values for the $os, $browser, and $device fields are can change from time to time.
    """
    import ast
    import inspect
    import re
    import discord

    def source(o):
        s = inspect.getsource(o).split("\n")
        indent = len(s[0]) - len(s[0].lstrip())

        return "\n".join(i[indent:] for i in s)

    source_ = source(discord.gateway.DiscordWebSocket.identify)
    patched = re.sub(
        r'([\'"]\$browser[\'"]:\s?[\'"]).+([\'"])',
        r"\1Discord Android\2",
        source_,
    )

    loc = {}
    exec(compile(ast.parse(patched), "<string>", "exec"), discord.gateway.__dict__, loc)
    return loc["identify"]

Now all there is left to do is overwrite the discord.gateway.DiscordWebSocket.identify during runtime in the main file, something like this :

import discord
import os
from discord.ext import commands
import mobile_status

discord.gateway.DiscordWebSocket.identify = mobile_status.get_mobile()
bot = commands.Bot(command_prefix="?")

@bot.event
async def on_ready():
    print(f"Sucessfully logged in as {bot.user}")

bot.run(os.getenv("DISCORD_TOKEN"))

And we do get the mobile status successfully
successful mobile status for bot

But here's the problem, I wanted to directly modify the file (which held the function) rather than monkey-patching it during runtime. So I cloned the dpy lib locally and edited the file on my machine, it ended up looking like this :

    async def identify(self):
        """Sends the IDENTIFY packet."""
        payload = {
            'op': self.IDENTIFY,
            'd': {
                'token': self.token,
                'properties': {
                    '$os': sys.platform,
                    '$browser': 'Discord Android',
                    '$device': 'Discord Android',
                    '$referrer': '',
                    '$referring_domain': ''
                },
                'compress': True,
                'large_threshold': 250,
                'v': 3
            }
        }
     # ...

(edited both $browser and $device to Discord Android just to be safe)

But this does not work and just gives me the regular desktop online icon.
So the next thing I did is to inspect the identify function after it has been monkey-patched, so I could just look at the source code and see what went wrong earlier, but due to hard luck I got this error :

Traceback (most recent call last):
  File "c:\Users\Achxy\Desktop\fresh\file.py", line 8, in <module>
    print(inspect.getsource(discord.gateway.DiscordWebSocket.identify))
  File "C:\Users\Achxy\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 1024, in getsource
    lines, lnum = getsourcelines(object)
  File "C:\Users\Achxy\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 1006, in getsourcelines
    lines, lnum = findsource(object)
  File "C:\Users\Achxy\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 835, in findsource
    raise OSError('could not get source code')
OSError: could not get source code

Code :

import discord
import os
from discord.ext import commands
import mobile_status
import inspect

discord.gateway.DiscordWebSocket.identify = mobile_status.get_mobile()
print(inspect.getsource(discord.gateway.DiscordWebSocket.identify))
bot = commands.Bot(command_prefix="?")

@bot.event
async def on_ready():
    print(f"Sucessfully logged in as {bot.user}")

bot.run(os.getenv("DISCORD_TOKEN"))

Since this same behavior was exhibited for every patched function (aforementioned one and the loc["identify"]) I could no longer use inspect.getsource(...) and then relied upon dis.dis which lead to much more disappointing results

The disassembled data looks exactly identical to the monkey-patched working version, so the directly modified version simply does not work despite function content being the exact same. (In regards to disassembled data)

Notes: Doing Discord iOS directly does not work either, changing the $device to some other value but keeping $browser does not work, I have tried all combinations, none of them work.

TL;DR: How to get mobile status for discord bot without monkey-patching it during runtime?


Solution

  • The following works by subclassing the relevant class, and duplicating code with the relevant changes. We also have to subclass the Client class, to overwrite the place where the gateway/websocket class is used. This results in a lot of duplicated code, however it does work, and requires neither dirty monkey-patching nor editing the library source code.

    However, it does come with many of the same problems as editing the library source code - mainly that as the library is updated, this code will become out of date (if you're using the archived and obsolete version of the library, you have bigger problems instead).

    import asyncio
    import sys
    
    import aiohttp
    
    import discord
    from discord.gateway import DiscordWebSocket, _log
    from discord.ext.commands import Bot
    
    
    class MyGateway(DiscordWebSocket):
    
        async def identify(self):
            payload = {
                'op': self.IDENTIFY,
                'd': {
                    'token': self.token,
                    'properties': {
                        '$os': sys.platform,
                        '$browser': 'Discord Android',
                        '$device': 'Discord Android',
                        '$referrer': '',
                        '$referring_domain': ''
                    },
                    'compress': True,
                    'large_threshold': 250,
                    'v': 3
                }
            }
    
            if self.shard_id is not None and self.shard_count is not None:
                payload['d']['shard'] = [self.shard_id, self.shard_count]
    
            state = self._connection
            if state._activity is not None or state._status is not None:
                payload['d']['presence'] = {
                    'status': state._status,
                    'game': state._activity,
                    'since': 0,
                    'afk': False
                }
    
            if state._intents is not None:
                payload['d']['intents'] = state._intents.value
    
            await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify)
            await self.send_as_json(payload)
            _log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id)
    
    
    class MyBot(Bot):
    
        async def connect(self, *, reconnect: bool = True) -> None:
            """|coro|
    
            Creates a websocket connection and lets the websocket listen
            to messages from Discord. This is a loop that runs the entire
            event system and miscellaneous aspects of the library. Control
            is not resumed until the WebSocket connection is terminated.
    
            Parameters
            -----------
            reconnect: :class:`bool`
                If we should attempt reconnecting, either due to internet
                failure or a specific failure on Discord's part. Certain
                disconnects that lead to bad state will not be handled (such as
                invalid sharding payloads or bad tokens).
    
            Raises
            -------
            :exc:`.GatewayNotFound`
                If the gateway to connect to Discord is not found. Usually if this
                is thrown then there is a Discord API outage.
            :exc:`.ConnectionClosed`
                The websocket connection has been terminated.
            """
    
            backoff = discord.client.ExponentialBackoff()
            ws_params = {
                'initial': True,
                'shard_id': self.shard_id,
            }
            while not self.is_closed():
                try:
                    coro = MyGateway.from_client(self, **ws_params)
                    self.ws = await asyncio.wait_for(coro, timeout=60.0)
                    ws_params['initial'] = False
                    while True:
                        await self.ws.poll_event()
                except discord.client.ReconnectWebSocket as e:
                    _log.info('Got a request to %s the websocket.', e.op)
                    self.dispatch('disconnect')
                    ws_params.update(sequence=self.ws.sequence, resume=e.resume, session=self.ws.session_id)
                    continue
                except (OSError,
                        discord.HTTPException,
                        discord.GatewayNotFound,
                        discord.ConnectionClosed,
                        aiohttp.ClientError,
                        asyncio.TimeoutError) as exc:
    
                    self.dispatch('disconnect')
                    if not reconnect:
                        await self.close()
                        if isinstance(exc, discord.ConnectionClosed) and exc.code == 1000:
                            # clean close, don't re-raise this
                            return
                        raise
    
                    if self.is_closed():
                        return
    
                    # If we get connection reset by peer then try to RESUME
                    if isinstance(exc, OSError) and exc.errno in (54, 10054):
                        ws_params.update(sequence=self.ws.sequence, initial=False, resume=True, session=self.ws.session_id)
                        continue
    
                    # We should only get this when an unhandled close code happens,
                    # such as a clean disconnect (1000) or a bad state (bad token, no sharding, etc)
                    # sometimes, discord sends us 1000 for unknown reasons so we should reconnect
                    # regardless and rely on is_closed instead
                    if isinstance(exc, discord.ConnectionClosed):
                        if exc.code == 4014:
                            raise discord.PrivilegedIntentsRequired(exc.shard_id) from None
                        if exc.code != 1000:
                            await self.close()
                            raise
    
                    retry = backoff.delay()
                    _log.exception("Attempting a reconnect in %.2fs", retry)
                    await asyncio.sleep(retry)
                    # Always try to RESUME the connection
                    # If the connection is not RESUME-able then the gateway will invalidate the session.
                    # This is apparently what the official Discord client does.
                    ws_params.update(sequence=self.ws.sequence, resume=True, session=self.ws.session_id)
    
    
    bot = MyBot(command_prefix="?")
    
    
    @bot.event
    async def on_ready():
        print(f"Sucessfully logged in as {bot.user}")
    
    bot.run("YOUR_BOT_TOKEN")
    

    Personally, I think that the following approach, which does include some runtime monkey-patching (but no AST manipulation) is cleaner for this purpose:

    import sys
    from discord.gateway import DiscordWebSocket, _log
    from discord.ext.commands import Bot
    
    
    async def identify(self):
        payload = {
            'op': self.IDENTIFY,
            'd': {
                'token': self.token,
                'properties': {
                    '$os': sys.platform,
                    '$browser': 'Discord Android',
                    '$device': 'Discord Android',
                    '$referrer': '',
                    '$referring_domain': ''
                },
                'compress': True,
                'large_threshold': 250,
                'v': 3
            }
        }
    
        if self.shard_id is not None and self.shard_count is not None:
            payload['d']['shard'] = [self.shard_id, self.shard_count]
    
        state = self._connection
        if state._activity is not None or state._status is not None:
            payload['d']['presence'] = {
                'status': state._status,
                'game': state._activity,
                'since': 0,
                'afk': False
            }
    
        if state._intents is not None:
            payload['d']['intents'] = state._intents.value
    
        await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify)
        await self.send_as_json(payload)
        _log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id)
    
    
    DiscordWebSocket.identify = identify
    bot = Bot(command_prefix="?")
    
    
    @bot.event
    async def on_ready():
        print(f"Sucessfully logged in as {bot.user}")
    
    bot.run("YOUR_DISCORD_TOKEN")
    

    As to why editing the library source code did not work for you, I can only assume that you have edited the wrong copy of the file, as people have commented.