Flux en continu

wss wss://ws.xsignalfeed.com/v1/stream

Se connecter

Le stream accepte votre clé des trois mêmes façons que REST. Depuis un serveur, utilisez un en-tête ; le paramètre d'URL seulement là où l'on ne peut pas définir d'en-têtes. Si la clé n'arrive pas ou n'est pas acceptée, la connexion s'ouvre puis se ferme aussitôt avec 4001, et la raison de fermeture indique pourquoi.

Comment envoyer la cléÀ utiliser quand
Authorization: Bearer xsf_…Recommandé pour les serveurs ; le même en-tête qu'en REST
X-API-Key: xsf_…Équivalent au précédent
?api_key=xsf_…Navigateurs, et clients qui ne peuvent pas définir d'en-têtes au handshake. La clé fait alors partie de l'URL : gardez ces URL hors des journaux
python — clé dans un en-tête
import websockets

async with websockets.connect("wss://ws.xsignalfeed.com/v1/stream",
                              additional_headers={"Authorization": "Bearer xsf_your_key"}) as ws:
    print(await ws.recv())    # {"t": "system.hello", ...}
Paramètre de requêteSignification
api_keyVotre clé, quand vous ne pouvez pas définir l'en-tête
last_idReprendre après ce sid. Ignoré en silence sauf si la clé possède la portée replay
import asyncio, json, websockets

async def main():
    url = "wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key"
    async with websockets.connect(url) as ws:
        async for raw in ws:
            ev = json.loads(raw)
            if ev["t"].startswith("system."): continue
            print(ev["t"], ev["target"]["screen_name"], ev["d"].get("text", "")[:60])

asyncio.run(main())

Trame

Trois trames viennent de nous plutôt que de X :

TrameQuand
system.helloPremière trame. targets indique combien de comptes cette clé surveille à cet instant (les comptes ajoutés ensuite rejoignent le flux sans reconnexion) ; replay_from renvoie le last_id avec lequel vous vous êtes reconnecté et vaut null si vous n'en avez pas envoyé, auquel cas rien n'est rejoué
system.pingEnvoyée pendant les silences comme signal de vie, pour que les proxys inactifs ne ferment pas le socket
system.errorQuelque chose ne va pas avec la connexion ; une fermeture suit

Code de fermeture

La poignée de main réussit toujours et la fermeture suit immédiatement, de sorte que votre bibliothèque cliente voit le code et la raison plutôt qu'une simple erreur HTTP.

Code de fermetureSignification
1012Redémarrage du service pendant une maintenance. Reconnectez-vous avec un délai croissant et passez le dernier sid traité en ?last_id= : les événements de l'interruption sont rejoués, rien n'est perdu
4001La clé n'a pas été acceptée. La raison de fermeture indique pourquoi : missing_api_key (aucune clé ne nous est parvenue : vérifiez le nom de l'en-tête, ou que votre proxy ou bibliothèque cliente le transmet), invalid_api_key (cette clé n'existe pas : faute de frappe, ou secret remplacé par une rotation), key_revoked (la clé a été révoquée)
4002Trop de connexions simultanées pour cette clé ; une trame system.error précède la fermeture
4003La clé ne possède pas la portée stream

Se reconnecter correctement

Écrivez ceci une fois, pas la version naïve. Les deux choses qui comptent sont le repli exponentiel et la persistance du sid quelque part qui survive à un redémarrage du processus.

python — version de production
import asyncio, json, pathlib, random, websockets

STATE = pathlib.Path("xsf.cursor")          # survives restarts; a DB row is better still

async def run(key, handle):
    backoff = 1
    while True:
        last = STATE.read_text().strip() if STATE.exists() else None
        url = f"wss://ws.xsignalfeed.com/v1/stream?api_key={key}" + (f"&last_id={last}" if last else "")
        try:
            async with websockets.connect(url, ping_interval=20, max_size=2**22) as ws:
                backoff = 1
                async for raw in ws:
                    ev = json.loads(raw)
                    if ev["t"].startswith("system."): continue
                    handle(ev)                      # must be idempotent: dedupe on ev["id"]
                    if ev.get("sid"): STATE.write_text(ev["sid"])   # only after handling
        except Exception as e:
            print("disconnected:", e)
        await asyncio.sleep(backoff + random.random())   # jitter, or every client returns at once
        backoff = min(60, backoff * 2)