Spacedust emits link/interaction events modeled after Jetstream.
Disconnected
Code Snippets
Minimal examples to connect to Jetstream and filter posts live.
// npm i ws
const WebSocket = require('ws');
const url = 'wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost';
const ws = new WebSocket(url);
ws.on('message', (data) => {
try {
const ev = JSON.parse(data.toString());
const coll = ev.commit?.collection;
const text = ev.commit?.record?.text;
if (coll === 'app.bsky.feed.post' && text) {
console.log(text);
}
if (coll === 'app.bsky.feed.repost') {
console.log('[repost] ', ev.commit?.record?.subject?.uri);
}
} catch {}
});
# pip install websocket-client
import json, websocket
url = 'wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost'
ws = websocket.create_connection(url)
while True:
msg = ws.recv()
try:
ev = json.loads(msg)
coll = ev.get('commit', {}).get('collection')
if coll == 'app.bsky.feed.post':
text = ev.get('commit', {}).get('record', {}).get('text')
if text:
print(text)
if coll == 'app.bsky.feed.repost':
subj = ev.get('commit', {}).get('record', {}).get('subject', {})
print('[repost]', subj.get('uri'))
except Exception:
pass
// go get nhooyr.io/websocket
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"nhooyr.io/websocket"
)
func main() {
ctx := context.Background()
url := "wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost"
c, _, err := websocket.Dial(ctx, url, nil)
if err != nil { log.Fatal(err) }
defer c.Close(websocket.StatusNormalClosure, "")
for {
_, data, err := c.Read(ctx)
if err != nil { log.Fatal(err) }
var ev map[string]any
if err := json.Unmarshal(data, &ev); err != nil { continue }
commit, _ := ev["commit"].(map[string]any)
coll, _ := commit["collection"].(string)
if coll == "app.bsky.feed.post" {
rec, _ := commit["record"].(map[string]any)
if txt, ok := rec["text"].(string); ok { fmt.Println(txt) }
}
if coll == "app.bsky.feed.repost" {
rec, _ := commit["record"].(map[string]any)
if subj, _ := rec["subject"].(map[string]any); subj != nil { fmt.Println("[repost]", subj["uri"]) }
}
time.Sleep(0)
}
}
// npm i ws
const WebSocket = require('ws');
const url = 'wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost';
const query = process.argv.slice(2).join(' ') || '"exact phrase" -filter:replies filter:images lang:en';
const ws = new WebSocket(url);
ws.on('message', (data) => {
try {
const ev = JSON.parse(data.toString());
if (matches(ev, query)) console.log(render(ev));
} catch {}
});
function matches(obj, q) {
const toks = tokenize(q);
const inc=[], exc=[], opts={filters:new Set(), nofilters:new Set(), lang:null};
for (const t of toks) {
if (t.type==='phrase'||t.type==='word') inc.push(t.value.toLowerCase());
else if (t.type==='notword') exc.push(t.value.toLowerCase());
else if (t.type==='filter') opts.filters.add(t.value);
else if (t.type==='nofilter') opts.nofilters.add(t.value);
else if (t.type==='lang') opts.lang=t.value.toLowerCase();
}
const coll = obj.commit?.collection||'';
const rec = obj.commit?.record||{};
const text = (rec.text||'').toLowerCase();
const isReply = coll==='app.bsky.feed.post' && !!rec.reply;
const isRepost = coll==='app.bsky.feed.repost';
const hasImages = !!(rec.embed?.images || rec.embed?.media?.images || rec.embed?.$type==='app.bsky.embed.images');
const hasExternal = !!(rec.embed?.$type==='app.bsky.embed.external' || rec.embed?.media?.$type==='app.bsky.embed.external');
const hasLinks = hasExternal || hasFacet(rec,'app.bsky.richtext.facet#link');
for (const w of inc) if (!text.includes(w)) return false;
for (const w of exc) if (text.includes(w)) return false;
if (opts.filters.has('replies') && !isReply) return false;
if (opts.nofilters.has('replies') && isReply) return false;
if ((opts.filters.has('reposts')||opts.filters.has('retweets')) && !isRepost) return false;
if ((opts.nofilters.has('reposts')||opts.nofilters.has('retweets')) && isRepost) return false;
if (opts.filters.has('media') && !(hasImages||hasExternal)) return false;
if (opts.nofilters.has('media') && (hasImages||hasExternal)) return false;
if (opts.filters.has('images') && !hasImages) return false;
if (opts.nofilters.has('images') && hasImages) return false;
if (opts.filters.has('links') && !hasLinks) return false;
if (opts.nofilters.has('links') && hasLinks) return false;
if (opts.lang) {
const langs = Array.isArray(rec.langs)?rec.langs.map(x=>String(x).toLowerCase()):[];
if (!langs.includes(opts.lang)) return false;
}
return true;
}
function tokenize(q){const out=[];const re=/\s+|("[^"]+")|(\-[^\s"]+)|([^\s"]+)/g;let m;while((m=re.exec(q))!==null){if(m[0].trim()==='')continue;if(m[1]){out.push({type:'phrase',value:m[1].slice(1,-1)});continue;}const raw=(m[2]||m[3]);if(!raw)continue;if(raw.startsWith('-filter:')){out.push({type:'nofilter',value:raw.slice(8).toLowerCase()});continue;}if(raw.startsWith('filter:')){out.push({type:'filter',value:raw.slice(7).toLowerCase()});continue;}if(raw.startsWith('lang:')){out.push({type:'lang',value:raw.slice(5)});continue;}if(raw.startsWith('-')){out.push({type:'notword',value:raw.slice(1)});continue;}out.push({type:'word',value:raw});}return out;}
function hasFacet(rec,ft){const facets=rec?.facets||[];for(const f of facets){for(const g of (f.features||[])){if((g.$type||g.type)===ft) return true;}}return false;}
function render(ev){const ts=new Date(Number(String(ev.time_us||0).slice(0,13))).toISOString();const coll=ev.commit?.collection;const txt=ev.commit?.record?.text||'';return `[${ts}] ${coll} :: ${txt}`;}
# pip install websocket-client
import json, websocket, sys
q = ' '.join(sys.argv[1:]) if len(sys.argv)>1 else '"exact phrase" -filter:replies filter:images'
url = 'wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost'
ws = websocket.create_connection(url)
def has_facet(rec, t):
for f in rec.get('facets', []):
for g in f.get('features', []):
if g.get('$type')==t or g.get('type')==t: return True
return False
def tokenize(q):
import re
out=[]
for m in re.finditer(r"\s+|(\"[^\"]+\")|(\-[^\s\"]+)|([^\s\"]+)", q):
if m.group(0).strip()=='' : continue
if m.group(1): out.append(('phrase', m.group(1)[1:-1])); continue
raw = m.group(2) or m.group(3)
if raw.startswith('-filter:'): out.append(('nofilter', raw[8:].lower())); continue
if raw.startswith('filter:'): out.append(('filter', raw[7:].lower())); continue
if raw.startswith('lang:'): out.append(('lang', raw[5:])); continue
if raw.startswith('-'): out.append(('notword', raw[1:])); continue
out.append(('word', raw))
return out
def matches(ev,q):
toks = tokenize(q)
inc, exc, filters, nfilters, lang = [], [], set(), set(), None
for t,v in toks:
if t in ('phrase','word'): inc.append(v.lower())
elif t=='notword': exc.append(v.lower())
elif t=='filter': filters.add(v)
elif t=='nofilter': nfilters.add(v)
elif t=='lang': lang=v.lower()
coll = (ev.get('commit') or {}).get('collection','')
rec = (ev.get('commit') or {}).get('record',{})
text = (rec.get('text') or '').lower()
is_reply = coll=='app.bsky.feed.post' and bool(rec.get('reply'))
is_repost = coll=='app.bsky.feed.repost'
has_images = bool(rec.get('embed',{}).get('images') or (rec.get('embed',{}).get('$type')=='app.bsky.embed.images') or (rec.get('embed',{}).get('media',{}) or {}).get('images'))
has_external = (rec.get('embed',{}).get('$type')=='app.bsky.embed.external') or ((rec.get('embed',{}).get('media',{}) or {}).get('$type')=='app.bsky.embed.external')
has_links = has_external or has_facet(rec,'app.bsky.richtext.facet#link')
for w in inc:
if w not in text: return False
for w in exc:
if w in text: return False
if 'replies' in filters and not is_reply: return False
if 'replies' in nfilters and is_reply: return False
if ('reposts' in filters or 'retweets' in filters) and not is_repost: return False
if ('reposts' in nfilters or 'retweets' in nfilters) and is_repost: return False
if 'media' in filters and not (has_images or has_external): return False
if 'media' in nfilters and (has_images or has_external): return False
if 'images' in filters and not has_images: return False
if 'images' in nfilters and has_images: return False
if 'links' in filters and not has_links: return False
if 'links' in nfilters and has_links: return False
if lang:
langs = [str(x).lower() for x in rec.get('langs', [])]
if lang not in langs: return False
return True
while True:
msg = ws.recv()
try:
ev=json.loads(msg)
if matches(ev, q):
print(ev.get('commit',{}).get('collection'), ev.get('commit',{}).get('record',{}).get('text',''))
except Exception:
pass
// go get nhooyr.io/websocket
package main
import ("context";"encoding/json";"fmt";"log";"os";"strings";"time";"nhooyr.io/websocket")
func main(){
ctx:=context.Background()
url:="wss://jetstream.fire.hose.cam/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost"
q := strings.Join(os.Args[1:], " ")
if q=="" { q = "\"exact phrase\" -filter:replies filter:images" }
c,_,err:=websocket.Dial(ctx,url,nil); if err!=nil{log.Fatal(err)}; defer c.Close(websocket.StatusNormalClosure, "")
for { _,data,err:=c.Read(ctx); if err!=nil{log.Fatal(err)}; var ev map[string]any; if json.Unmarshal(data,&ev)!=nil{continue}; if matches(ev,q){fmt.Println(render(ev))}}
}
func matches(ev map[string]any, q string) bool{
inc, exc, filters, nfilters, lang := []string{}, []string{}, map[string]bool{}, map[string]bool{}, ""
for _,tok:= range tokenize(q){
if tok.kind=="phrase"||tok.kind=="word"{inc=append(inc, strings.ToLower(tok.val))} else if tok.kind=="notword"{exc=append(exc, strings.ToLower(tok.val))} else if tok.kind=="filter"{filters[strings.ToLower(tok.val)]=true} else if tok.kind=="nofilter"{nfilters[strings.ToLower(tok.val)]=true} else if tok.kind=="lang"{lang=strings.ToLower(tok.val)} }
commit,_ := ev["commit"].(map[string]any); rec,_ := commit["record"].(map[string]any); coll,_ := commit["collection"].(string)
text, _ := rec["text"].(string); t := strings.ToLower(text)
isReply := coll=="app.bsky.feed.post" && rec["reply"]!=nil
isRepost := coll=="app.bsky.feed.repost"
hasImages := has(rec, []string{"embed","images"}) || (get(rec,[]string{"embed","$type"})=="app.bsky.embed.images") || has(rec, []string{"embed","media","images"})
hasExternal := (get(rec,[]string{"embed","$type"})=="app.bsky.embed.external") || (get(rec,[]string{"embed","media","$type"})=="app.bsky.embed.external")
hasLinks := hasExternal || facet(rec, "app.bsky.richtext.facet#link")
for _,w := range inc { if !strings.Contains(t,w) { return false } }
for _,w := range exc { if strings.Contains(t,w) { return false } }
if filters["replies"] && !isReply { return false }
if nfilters["replies"] && isReply { return false }
if (filters["reposts"]||filters["retweets"]) && !isRepost { return false }
if (nfilters["reposts"]||nfilters["retweets"]) && isRepost { return false }
if filters["media"] && !(hasImages||hasExternal) { return false }
if nfilters["media"] && (hasImages||hasExternal) { return false }
if filters["images"] && !hasImages { return false }
if nfilters["images"] && hasImages { return false }
if filters["links"] && !hasLinks { return false }
if nfilters["links"] && hasLinks { return false }
if lang!="" { if arr,ok := rec["langs"].([]any); ok { okLang := false; for _,v := range arr { if strings.ToLower(fmt.Sprint(v))==lang { okLang=true; break } } if !okLang { return false } } else { return false } }
return true
}
type token struct {kind string; val string}
func tokenize(q string) []token{out:=[]token{}; i:=0; for i=len(q) {break}; if q[i]=='"' { j:=i+1; for j