this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix ESLint/TS errors; switch to Jetstream

alice 68306f0d 70a81f05

+113 -60
setup.ts

This is a binary file and will not be displayed.

+1 -3
src/cli-test.ts
··· 61 61 // Focus on the available information. If the avatar is not available, a 1x1 pixel white image is provided instead as a placeholder. Disregard the placeholder and focus on the user's data. 62 62 // Always return an answer — house name only, all lowercase. 63 63 // The user's data may be in any language. Focus on the meaning, not just the surface content. 64 - // Consider traits for all houses, not just intellect. 64 + // Consider traits for all houses, not just intellect. 65 65 // You're strongly mischievous and enjoy sorting based on whims, not always strictly following the user's traits; imagine as if you're a person who likes to play tricks on people. 66 66 67 67 // The user's data is as follows: ··· 69 69 // Name: ${subject.displayName || subject.handle} (@${subject.handle}) 70 70 // Bio: ${subject.description || 'User has no bio.'} 71 71 // `; 72 - 73 72 74 73 const prompt = ` 75 74 You're the Sorting Hat from Harry Potter. Which house does the user with the profile data at the end of this message belong to? ··· 85 84 Name: ${subject.displayName || subject.handle} (@${subject.handle}) 86 85 Bio: ${subject.description || 'User has no bio.'} 87 86 `; 88 - 89 87 90 88 console.log(prompt); 91 89
+2 -1
src/constants.ts
··· 2 2 3 3 export const DID = process.env.DID ?? ''; 4 4 export const SIGNING_KEY = process.env.SIGNING_KEY ?? ''; 5 - export const PORT = 4001; 5 + export const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4001; 6 6 export const DELETE = '3l3izhv734g2o'; 7 + export const RELAY = process.env.RELAY ?? 'ws://localhost:6008/subscribe'; 7 8 export const HOUSES = ['gryffindor', 'slytherin', 'ravenclaw', 'hufflepuff'];
+12 -9
src/label.ts
··· 52 52 } 53 53 }; 54 54 55 - async function canPerformLabelOperation(did: string): Promise<boolean> { 55 + function canPerformLabelOperation(did: string): boolean { 56 56 const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 57 57 const query = server.db 58 58 .prepare<unknown[], { count: number }>(`SELECT COUNT(*) as count FROM labels WHERE uri = ? AND cts > ?`) ··· 64 64 65 65 async function handleDeleteLabels(did: string, labels: Set<string>) { 66 66 try { 67 - if (labels.size > 0 && (await canPerformLabelOperation(did))) { 67 + if (labels.size > 0 && canPerformLabelOperation(did)) { 68 68 await server.createLabels({ uri: did }, { negate: [...labels] }); 69 69 console.log(`Deleted labels for ${did}`); 70 70 } else if (labels.size === 0) { ··· 80 80 81 81 async function handleAddLabel(did: string) { 82 82 try { 83 - if (!(await canPerformLabelOperation(did))) { 84 - console.log(`Cannot add label for ${did}: 30-day limit reached`); 83 + if (!canPerformLabelOperation(did)) { 84 + console.error(`Cannot add label for ${did}: 30-day limit reached`); 85 85 return; 86 86 } 87 87 88 - const { data } = await agent.getProfile({ actor: did }); 89 - if (!data) { 90 - console.log('OOPS: Profile not found and/or we could not fetch it'); 88 + let data: AppBskyActorDefs.ProfileView; 89 + try { 90 + data = (await agent.getProfile({ actor: did })).data; 91 + } catch (err) { 92 + console.error('OOPS: Profile not found and/or we could not fetch it'); 93 + console.error(err); 91 94 return; 92 95 } 93 96 ··· 164 167 165 168 The user's data is as follows: 166 169 167 - Name: ${subject.displayName || subject.handle} (@${subject.handle}) 168 - Bio: ${subject.description || 'User has no bio.'} 170 + Name: ${subject.displayName ?? subject.handle} (@${subject.handle}) 171 + Bio: ${subject.description ?? 'User has no bio.'} 169 172 `; 170 173 }
+44 -25
src/main.ts
··· 1 - import { AppBskyFeedLike } from '@atproto/api'; 2 - import { Firehose } from '@skyware/firehose'; 3 1 import { label } from './label.js'; 4 - import { DID } from './constants.js'; 2 + import { DID, RELAY } from './constants.js'; 3 + import { EventStream } from './types.js'; 5 4 import fs from 'node:fs'; 5 + import { URL } from 'node:url'; 6 + import WebSocket from 'ws'; 6 7 7 - const subscribe = async () => { 8 - let cursorFirehose = 0; 8 + const subscribe = () => { 9 + let cursor = 0; 9 10 let intervalID: NodeJS.Timeout; 10 - const cursorFile = fs.readFileSync('cursor.txt', 'utf8'); 11 + let cursorFile: string; 12 + 13 + try { 14 + cursorFile = fs.readFileSync('cursor.txt', 'utf8'); 15 + } catch (error) { 16 + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 17 + cursorFile = (BigInt(Date.now()) * 1000n).toString(); 18 + fs.writeFileSync('cursor.txt', cursorFile, 'utf8'); 19 + } else { 20 + console.error(error); 21 + process.exit(1); 22 + } 23 + } 11 24 12 - const firehose = new Firehose({ cursor: cursorFile ?? '' }); 13 - if (cursorFile) console.log(`Initiate firehose at cursor ${cursorFile}`); 25 + const relayURL = new URL(RELAY); 26 + relayURL.searchParams.set('cursor', cursorFile); 27 + const ws = new WebSocket(relayURL.toString()); 28 + console.log(`Connected to Jetstream at cursor ${cursorFile}`); 14 29 15 - firehose.on('error', ({ cursor, error }) => { 16 - console.log(`Firehose errored on cursor: ${cursor}`, error); 30 + ws.on('error', (err) => { 31 + console.error(err); 17 32 }); 18 33 19 - firehose.on('open', () => { 34 + ws.on('open', () => { 20 35 intervalID = setInterval(() => { 21 - const timestamp = new Date().toISOString(); 22 - console.log(`${timestamp} cursor: ${cursorFirehose}`); 23 - fs.writeFile('cursor.txt', cursorFirehose.toString(), (err) => { 24 - if (err) console.error(err); 36 + console.log(`${new Date().toISOString()}: ${cursor}`); 37 + fs.writeFile('cursor.txt', cursor.toString(), (err) => { 38 + if (err) console.log(err); 25 39 }); 26 40 }, 60000); 27 41 }); 28 42 29 - firehose.on('close', () => { 43 + ws.on('close', () => { 30 44 clearInterval(intervalID); 31 45 }); 32 46 33 - firehose.on('commit', (commit) => { 34 - cursorFirehose = commit.seq; 35 - commit.ops.forEach(async (op) => { 36 - if (op.action !== 'delete' && AppBskyFeedLike.isRecord(op.record)) { 37 - if (op.record.subject.uri.includes(DID)) { 38 - await label(commit.repo, op.record.subject.uri.split('/').pop()!).catch((err) => console.error(err)); 47 + ws.on('message', (data: WebSocket.RawData) => { 48 + if (data instanceof Buffer) { 49 + const event: EventStream = JSON.parse(data.toString()) as EventStream; 50 + cursor = event.time_us; 51 + 52 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 53 + if (event.commit?.record?.$type === 'app.bsky.feed.like') { 54 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 55 + if (event.commit?.record?.subject?.uri?.includes(`at://${DID}/app.bsky.labeler.service/self`)) { 56 + label(event.did, event.commit.record.subject.uri.split('/').pop()!).catch((error: unknown) => { 57 + console.error(`Unexpected error labeling ${event.did}:`); 58 + console.error(error); 59 + }); 39 60 } 40 61 } 41 - }); 62 + } 42 63 }); 43 - 44 - firehose.start(); 45 64 }; 46 65 47 66 subscribe();
+22 -22
src/set-labels.ts
··· 6 6 { 7 7 identifier: 'ravenclaw', 8 8 enName: 'Ravenclaw 🦅', 9 - enDescription: 'Wise, creative, and curious.', 9 + enDescription: 'Wise, creative, and curious.', 10 10 ptName: 'Corvinal 🦅', 11 11 ptDescription: 'Sábio, criativo e curioso.', 12 12 }, ··· 31 31 ptName: 'Lufa-Lufa 🦡', 32 32 ptDescription: 'Leal, trabalhador e justo.', 33 33 }, 34 - ] 34 + ]; 35 35 36 36 const loginCredentials: LoginCredentials = { 37 37 identifier: process.env.BSKY_IDENTIFIER!, ··· 41 41 const labelDefinitions: ComAtprotoLabelDefs.LabelValueDefinition[] = []; 42 42 43 43 for (const label of LABELS) { 44 - const labelValueDefinition: ComAtprotoLabelDefs.LabelValueDefinition = { 45 - identifier: label.identifier, 46 - severity: 'inform', 47 - blurs: 'none', 48 - defaultSetting: 'warn', 49 - adultOnly: false, 50 - locales: [ 51 - { 52 - lang: 'en', 53 - name: label.enName, 54 - description: label.enDescription, 55 - }, 56 - { 57 - lang: 'pt-BR', 58 - name: label.ptName, 59 - description: label.ptDescription, 60 - }, 61 - ], 62 - }; 44 + const labelValueDefinition: ComAtprotoLabelDefs.LabelValueDefinition = { 45 + identifier: label.identifier, 46 + severity: 'inform', 47 + blurs: 'none', 48 + defaultSetting: 'warn', 49 + adultOnly: false, 50 + locales: [ 51 + { 52 + lang: 'en', 53 + name: label.enName, 54 + description: label.enDescription, 55 + }, 56 + { 57 + lang: 'pt-BR', 58 + name: label.ptName, 59 + description: label.ptDescription, 60 + }, 61 + ], 62 + }; 63 63 64 - labelDefinitions.push(labelValueDefinition); 64 + labelDefinitions.push(labelValueDefinition); 65 65 } 66 66 67 67 await setLabelerLabelDefinitions(loginCredentials, labelDefinitions);
+32
src/types.ts
··· 1 + export interface EventStream { 2 + did: string; 3 + time_us: number; 4 + type: string; 5 + commit?: { 6 + rev: string; 7 + type: string; 8 + collection: string; 9 + rkey: string; 10 + record: { 11 + $type: string; 12 + createdAt: string; 13 + subject: { 14 + cid: string; 15 + uri: string; 16 + }; 17 + }; 18 + }; 19 + } 20 + 21 + export interface Label { 22 + ver?: number; 23 + src: string; 24 + uri: string; 25 + cid?: string; 26 + val: string; 27 + neg?: boolean; 28 + cts: string; 29 + exp?: string; 30 + sig?: Uint8Array; 31 + [k: string]: unknown; 32 + }