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 |
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ête | Signification |
|---|---|
| api_key | Votre clé, quand vous ne pouvez pas définir l'en-tête |
| last_id | Reprendre 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())
import WebSocket from "ws"; const ws = new WebSocket("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key"); ws.on("message", (raw) => { const ev = JSON.parse(raw); if (ev.t.startsWith("system.")) return; console.log(ev.t, ev.target.screen_name, (ev.d.text ?? "").slice(0, 60)); });
c, _, err := websocket.DefaultDialer.Dial("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key", nil) for { _, raw, err := c.ReadMessage() if err != nil { break } var ev Event json.Unmarshal(raw, &ev) if strings.HasPrefix(ev.T, "system.") { continue } log.Println(ev.T, ev.Target.ScreenName) }
Trame
Trois trames viennent de nous plutôt que de X :
| Trame | Quand |
|---|---|
system.hello | Premiè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.ping | Envoyée pendant les silences comme signal de vie, pour que les proxys inactifs ne ferment pas le socket |
system.error | Quelque 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 fermeture | Signification |
|---|---|
1012 | Redé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 |
4001 | La 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) |
4002 | Trop de connexions simultanées pour cette clé ; une trame system.error précède la fermeture |
4003 | La 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.
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)