显然,discord机器人可以拥有移动状态,而不是默认的桌面(在线)状态.

bot having mobile status

经过一点挖掘,我发现这样的状态是通过修改IDENTIFY packet/discord.gateway.DiscordWebSocket.identify来实现的,修改$browserDiscord AndroidDiscord iOS的值理论上应该可以得到移动状态.

在修改了我在网上找到的代码片段后,我得到了以下结论:

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"]

现在只需在运行时在主文件中覆盖discord.gateway.DiscordWebSocket.identify,如下所示:

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,我想直接修改文件(保存函数),而不是在运行时对其进行修补.所以我在本地克隆了dpy库,并在我的机器上编辑了文件,结果是这样的:

    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
            }
        }
     # ...

(为了安全起见,编辑了$browser$deviceDiscord Android)

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

代码:

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"))

由于每个补丁函数(前面提到的一个和loc["identify"]个)都表现出了相同的行为,我不能再使用inspect.getsource(...),然后依赖dis.dis,这导致了更令人失望的结果

分解后的数据看起来与猴子补丁的工作版本完全相同,因此直接修改的版本根本无法工作,尽管函数内容完全相同.(关于反汇编数据)

注意:直接使用Discord iOS也不起作用,将$device改为其他值,但保留$browser不起作用,我try 了所有组合,但都不起作用.

TL;DR:如何在运行时不使用猴子补丁的情况下获取discord机器人的移动状态?

推荐答案

下面的工作是对相关类进行子类化,并使用相关更改复制代码.我们还必须对Client类进行子类化,以覆盖gateway/websocket类的使用位置.这会导致大量重复的代码,但它确实可以工作,并且不需要脏猴子补丁,也不需要编辑库源代码.

然而,它确实会带来许多与编辑库源代码相同的问题——主要是随着库的更新,这些代码将变得过时(如果您使用的是库的存档和过时版本,则会出现更大的问题).

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")

就我个人而言,我认为以下方法(包括一些运行时猴子补丁(但没有AST操作))更干净:

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")

至于为什么编辑库源代码对您不起作用,我只能假设您编辑了错误的文件副本,正如人们所 comments 的那样.

Python相关问答推荐

如何将带有逗号分隔的数字的字符串解析为int Array?

Python中MongoDB的BSON时间戳

多处理代码在while循环中不工作

为什么tkinter框架没有被隐藏?

按顺序合并2个词典列表

如何在虚拟Python环境中运行Python程序?

我们可以为Flask模型中的id字段主键设置默认uuid吗

OR—Tools CP SAT条件约束

把一个pandas文件夹从juyter笔记本放到堆栈溢出问题中的最快方法?

joblib:无法从父目录的另一个子文件夹加载转储模型

我的字符串搜索算法的平均时间复杂度和最坏时间复杂度是多少?

在Python中计算连续天数

OpenCV轮廓.很难找到给定图像的所需轮廓

用两个字符串构建回文

Beautifulsoup:遍历一个列表,从a到z,并解析数据,以便将其存储在pdf中.

从一个df列提取单词,分配给另一个列

为罕见情况下的回退None值键入

如何写一个polars birame到DuckDB

在round函数中使用列值

以极轴表示的行数表达式?