-
-
Save mxve/5cf8c58d5e68c59f0d8d6b91ef448ee1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const web_socket = require('ws'); | |
| // ?? | |
| const handle = ''; | |
| // password lol (app password should work ig) | |
| const password = ''; | |
| // pds urL | |
| const pds = ''; | |
| // watch for actions on this user's did | |
| const watched_user = ''; | |
| // enable/disable what triggers a follow, matched against collections | |
| const triggers = ['follow', 'like', 'repost']; | |
| // alternative jetstreams | |
| // "wss://jetstream1.us-east.bsky.network/subscribe", | |
| // "wss://jetstream2.us-east.bsky.network/subscribe", | |
| // "wss://jetstream1.us-west.bsky.network/subscribe", | |
| // "wss://jetstream2.us-west.bsky.network/subscribe", | |
| const jetstream = 'wss://jetstream2.us-east.bsky.network/subscribe'; | |
| // jetstream replay in secs | |
| const replay = 30; | |
| // stats log interval in secs | |
| const stats_interval = 5 | |
| // mapping triggers to actual collections | |
| const collections = { | |
| follow: 'app.bsky.graph.follow', | |
| like: 'app.bsky.feed.like', | |
| repost: 'app.bsky.feed.repost', | |
| }; | |
| let jwt, my_did, watched_did, last_cursor; | |
| let following = new Set(); | |
| let stats = { | |
| events: 0, | |
| follows: 0 | |
| }; | |
| // spam stats | |
| setInterval(() => { | |
| console.log(`${(stats.events / stats_interval).toFixed(1)} events/s — ${stats.follows} follows total — following ${following.size}`); | |
| stats.events = 0; | |
| }, stats_interval * 1000); | |
| // pds: authenticate | |
| async function pds_auth() { | |
| const res = await fetch( | |
| `${pds}/xrpc/com.atproto.server.createSession`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| identifier: handle, | |
| password | |
| }), | |
| }); | |
| return res.json(); | |
| } | |
| // pds: resolve did from handle | |
| async function pds_resolve_handle(handle) { | |
| const res = await fetch(`${pds}/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`); | |
| return res.json(); | |
| } | |
| // pds: create a new record | |
| async function pds_create_record(collection, record) { | |
| const res = await fetch( | |
| `${pds}/xrpc/com.atproto.repo.createRecord`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${jwt}` | |
| }, | |
| body: JSON.stringify({ | |
| repo: my_did, | |
| collection, | |
| record | |
| }), | |
| }); | |
| if (res.status === 401) { | |
| ({ | |
| accessJwt: jwt, | |
| did: my_did | |
| } = await pds_auth()); | |
| return pds_create_record(collection, record); | |
| } | |
| return res.json(); | |
| } | |
| // pds: create new follow record | |
| async function pds_create_record_follow(did) { | |
| if (following.has(did) || did === my_did || did === watched_did) { | |
| return; | |
| } | |
| following.add(did); | |
| await pds_create_record('app.bsky.graph.follow', { | |
| $type: 'app.bsky.graph.follow', | |
| subject: did, | |
| createdAt: new Date().toISOString(), | |
| }); | |
| stats.follows++; | |
| console.log('followed', did); | |
| } | |
| // bsky: get current follows | |
| async function bsky_get_following() { | |
| const result = new Set(); | |
| let cursor; | |
| do { | |
| const url = `https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${my_did}&limit=100` + (cursor ? `&cursor=${cursor}` : ''); | |
| const res = await fetch(url); | |
| const { follows, cursor: next } = await res.json(); | |
| for (const follow of follows ?? []) { | |
| result.add(follow.did); | |
| } | |
| cursor = next ?? null; | |
| } while (cursor); | |
| console.log(`loaded ${result.size} existing follows`); | |
| return result; | |
| } | |
| // jetstream: filter & validate events | |
| function on_jetstream_event(e) { | |
| if (e.kind !== 'commit' || e.commit.operation !== 'create') { | |
| return; | |
| } | |
| const { | |
| collection: col, | |
| record | |
| } = e.commit; | |
| const is_follow = col === collections.follow && | |
| triggers.includes('follow') && | |
| record.subject === watched_did; | |
| const is_like = col === collections.like && | |
| triggers.includes('like') && | |
| record.subject?.uri?.startsWith(`at://${watched_did}/`); | |
| const is_repost = col === collections.repost && | |
| triggers.includes('repost') && record.subject?.uri?.startsWith(`at://${watched_did}/`); | |
| if (is_follow || is_like || is_repost) { | |
| pds_create_record_follow(e.did).catch(console.error); | |
| } | |
| } | |
| // connect to the jetstream weeeeeeeeeeeeeeeeeeeeee | |
| function jetstream_connect() { | |
| const params = new URLSearchParams(); | |
| for (const trigger of triggers) { | |
| params.append('wantedCollections', collections[trigger]); | |
| } | |
| if (last_cursor) { | |
| params.set('cursor', String(last_cursor - 2_000_000)); | |
| } | |
| const ws = new web_socket(`${jetstream}?${params}`); | |
| ws.on('open', () => console.log('connected')); | |
| ws.on('close', () => setTimeout(connect, 5000)); | |
| ws.on('error', e => { | |
| console.error(e.message); | |
| ws.terminate(); | |
| }); | |
| ws.on('message', d => { | |
| try { | |
| const e = JSON.parse(d.toString()); | |
| if (e.time_us) { | |
| last_cursor = e.time_us; | |
| } | |
| stats.events++; | |
| on_jetstream_event(e); | |
| } catch { } | |
| }); | |
| } | |
| // Der Main ist mit 527 Kilometern Fließstrecke der längste rechte Nebenfluss des Rheins. Die Quellflüsse des Mains entspringen im Fichtelgebirge (Weißer Main) und in der Fränkischen Alb (Roter Main). Am westlichen Rand der Stadt Kulmbach im Stadtteil Melkendorf nahe dem Schloss Steinenhausen vereinigen sich die beiden Quellflüsse zum eigentlichen Main. Der Flusslauf hält trotz vieler markanter Richtungswechsel seine – in Mitteleuropa seltene – Hauptfließrichtung von Ost nach West bei und berührt dabei mehrere fränkische Mittelgebirge. Am Main liegen große Teile des fränkischen Weinbaugebiets und zahlreiche, teils gut erhaltene historische Stadtkerne. Große Ballungsräume durchfließt der Main um Würzburg und Frankfurt. Gegenüber der Mainzer Altstadt – zwischen Ginsheim-Gustavsburg und der Maaraue in Mainz-Kostheim – mündet er in den Rhein. Von dort (Kilometer 0) flussaufwärts bis oberhalb der Eisenbahnbrücke bei Hallstadt (Kilometer 387,69) ist der Main (Ma) Bundeswasserstraße. Der Main ist nicht ausnehmend lang, aber gleichwohl ein historisch und geografisch bedeutender Fluss. In der Spätantike bildete sein Unterlauf zwischen Miltenberg und Großkrotzenburg ein kurzes Stück der Außengrenze der Provinz Obergermanien des Römischen Reichs. Die Mainlinie trennte im 19. Jahrhundert die Einflusssphären der beiden deutschen Großmächte Österreich und Preußen innerhalb des Deutschen Bundes mit seinem Sitz in Frankfurt unmittelbar am Main. Der tatsächlich über weite Strecken innerhalb Oberdeutschlands verlaufende Main ist zwar weder eine Dialekt-, noch eine Kulturgrenze, doch gliedert nach häufigem Verständnis die Mainlinie Deutschland in einen nördlichen und südlichen Teil. Das Rhein-Main-Gebiet ist verkehrsgeographisch die Mitte Deutschlands und Europas. | |
| async function main() { | |
| ({ accessJwt: jwt, did: my_did } = await pds_auth()); | |
| following = await bsky_get_following(); | |
| const resolve_data = await pds_resolve_handle(watched_user); | |
| watched_did = resolve_data.did; | |
| console.log('watching', watched_did); | |
| last_cursor = Date.now() * 1000 - replay * 1_000_000; | |
| jetstream_connect(); | |
| } | |
| main().catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment