Guide technique

Mettre son bot Discord en casque VR

L'icône casque à côté du nom d'un bot n'existe dans aucune documentation officielle, et la documentation communautaire affirme même qu'elle est hors de portée. C'est faux : trois lignes de properties suffisent. Voici lesquelles, et pourquoi ça marche.

discord.py Gateway Testé en live 10 minutes

Ce que Discord affiche vraiment

À côté du nom d'un utilisateur, Discord peut afficher une petite icône de plateforme : un téléphone, une manette, un casque VR. Beaucoup de développeurs pensent que Discord détecte cette plateforme tout seul. Ce n'est pas le cas.

Presque tout ce qu'affiche Discord à propos d'une session, c'est le client qui l'envoie. Le serveur se contente de le relayer aux autres membres. Le statut vient du champ status de la présence, le point violet du stream vient du type d'activité, et l'icône de plateforme vient d'un endroit que la plupart des bibliothèques ne laissent jamais toucher : le champ properties du paquet IDENTIFY, envoyé une seule fois, à l'ouverture de la connexion gateway.

discord.py y écrit "discord.py" en dur. Résultat : un bot apparaît toujours comme un client bureau, quoi qu'on fasse ensuite avec change_presence().

Retenir la distinction : la présence (statut, activité) peut changer à tout moment. Les properties ne partent qu'au moment de l'IDENTIFY. Ce sont deux couches différentes.

Pourquoi la doc dit que c'est impossible

La documentation reverse-engineered de l'API (celle que tout le monde consulte quand la doc officielle est muette) classe le statut vr comme accessible uniquement via des « headless sessions », créées avec un jeton OAuth portant le scope activities.write — un scope privé, réservé à Meta. Conclusion logique : inatteignable pour un bot.

Sauf que ce mécanisme a bougé. Vers février 2026, Discord a reclassé les clients Android tournant sur les casques Meta : ils ne remontent plus dans le slot mobile mais dans le slot vr. L'application Discord native sur Quest, elle, n'est arrivée qu'en juin 2026 — après. Le mécanisme est donc récent, absent de l'API publique, et la documentation communautaire décrit encore l'état d'avant.

En pratique, la classification se fait sur la chaîne envoyée dans properties. Un client qui s'annonce comme un Discord tournant en VR sur Android est rangé dans le slot VR. Un bot peut envoyer exactement la même chose.

La leçon vaut au-delà de cet exemple : sur les mécanismes non documentés de Discord, un test de dix minutes bat une page de documentation non datée. Ce qui était vrai l'an dernier ne l'est plus forcément.

Les valeurs qui marchent

Chaque ligne correspond au contenu du champ properties envoyé dans l'IDENTIFY. Testées une par une en conditions réelles, en observant le rendu sur un vrai client Discord.

Icône obtenue os browser / device
Aucune (client bureau) win32, linux Discord Client
Téléphone vert android Discord Android
Casque VR android Discord VR
Aucune Toute chaîne non reconnue (Discord Quest, etc.)
Une chaîne non reconnue ne provoque aucune erreur : la connexion s'ouvre normalement, il ne se passe simplement rien. Pas de message, pas d'exception, pas d'icône. C'est ce qui rend la recherche pénible — il faut regarder le client à chaque essai.

Le code

discord.py n'expose pas les properties. On remplace donc la méthode DiscordWebSocket.identify par la nôtre. Le corps est une copie conforme de l'originale — seul le bloc properties change.

presence.py
import discord
import discord.gateway

# Les properties envoyées dans l'IDENTIFY : c'est ce trio qui
# fait ranger la session dans le slot "vr" côté Discord.
PROPERTIES_VR = {
    "os": "android",
    "browser": "Discord VR",
    "device": "Discord VR",
}


def installer_patch_identify():
    """Remplace identify() pour injecter nos properties.

    Copie conforme de l'originale de discord.py 2.7.1 : si tu es
    sur une autre version, recopie le corps de TA version et ne
    change que le bloc properties. À appeler avant bot.run().
    """

    async def identify(self):
        payload = {
            "op": self.IDENTIFY,
            "d": {
                "token": self.token,
                "properties": PROPERTIES_VR,   # <-- la seule ligne qui change
                "compress": True,
                "large_threshold": 250,
            },
        }

        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)

    discord.gateway.DiscordWebSocket.identify = identify

Côté lancement, il n'y a rien de plus à faire que d'appeler le patch avant de connecter le bot :

bot.py
from presence import installer_patch_identify

installer_patch_identify()   # AVANT bot.run(), sinon l'IDENTIFY est déjà parti
bot.run(TOKEN)
Le patch touche une API interne de discord.py (_connection, _intents). C'est le prix à payer, et ça implique de revérifier le code à chaque mise à jour de la bibliothèque. En contrepartie, le patch est sans effet de bord : il n'ajoute rien au payload, il remplace juste une valeur codée en dur.

Les trois pièges

PIÈGE 01

Redémarrage obligatoire

L'IDENTIFY ne part qu'à l'ouverture de connexion. Une simple reconnexion ne suffit pas : discord.py envoie alors un RESUME, pas un IDENTIFY. Changer de plateforme exige de relancer le process.

PIÈGE 02

Le bot est aveugle

Discord ne renvoie jamais à un bot sa propre présence : zéro PRESENCE_UPDATE, même avec l'intent presences, même depuis une seconde session du même token. Aucun script ne peut vérifier son propre affichage — il faut regarder le client.

PIÈGE 03

L'échec est silencieux

Une valeur non reconnue ne lève rien du tout. Même chose pour le point violet du stream : une URL hors twitch.tv / youtube.com retombe sans bruit en « joue à… » vert.

Et l'icône manette ?

La question tombe toujours juste après. L'icône manette correspond au slot embedded, celui des activités lancées dans Discord même. Aucune valeur de properties testée ne l'a déclenchée, et l'argument empirique est solide : l'intégration console est bien plus ancienne que la VR, des milliers de personnes ont cherché, personne n'a publié de méthode. Si le slot était atteignable côté client, ça se saurait.

La différence avec la VR tient précisément au reclassement de février 2026 : Discord y range des sessions Android ordinaires, celles que n'importe quel client — donc n'importe quel bot — peut imiter. Rien d'équivalent n'existe pour embedded.

À éviter : les contournements qui usurpent le client ID de Meta via un flow OAuth sur un compte utilisateur. C'est du self-botting, contraire aux conditions d'utilisation, passible de bannissement du compte — et sans le moindre effet sur un bot. La méthode décrite ici n'utilise, elle, qu'un jeton de bot normal et un champ que le client envoie de toute façon.

Testez, ne croyez pas la doc sur parole

Cette icône était classée « impossible » par la seule documentation qui en parlait. Il aura suffi d'essayer les chaînes une par une, et de regarder le client, pour que le casque apparaisse. Si vous tombez sur d'autres slots atteignables, venez le partager : c'est exactement le genre de trouvaille qui circule mal ailleurs.

En parler sur DevHub