this repo has no description
0
fork

Configure Feed

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

here goes nothing, trulyC

alice 3bff3658 89880c69

+135 -14
+135 -14
src/label.ts
··· 1 1 import { AppBskyActorDefs, ComAtprotoLabelDefs } from '@atproto/api'; 2 - import { DID, PORT, HOUSES, SIGNING_KEY, DELETE } from './constants.js'; 2 + import { DID, PORT, SIGNING_KEY, DELETE } from './constants.js'; 3 3 import { LabelerServer } from '@skyware/labeler'; 4 + import { generateText, tool } from 'ai'; 5 + import { openai } from '@ai-sdk/openai'; 6 + import { z } from 'zod'; 7 + import { createCanvas, loadImage } from 'canvas'; 8 + import { AtpAgent } from '@atproto/api'; 9 + import fs from 'node:fs/promises'; 10 + import 'dotenv/config'; 4 11 5 12 const server = new LabelerServer({ did: DID, signingKey: SIGNING_KEY }); 6 13 ··· 12 19 } 13 20 }); 14 21 22 + const agent = new AtpAgent({ 23 + service: process.env.BSKY_SERVICE ?? 'https://bsky.social', 24 + }); 25 + 26 + await agent.login({ 27 + identifier: process.env.BSKY_IDENTIFIER!, 28 + password: process.env.BSKY_PASSWORD!, 29 + }); 30 + 15 31 export const label = async (subject: string | AppBskyActorDefs.ProfileView, rkey: string) => { 16 32 const did = AppBskyActorDefs.isProfileView(subject) ? subject.did : subject; 17 33 34 + console.log(`Processing label for ${did}`); 35 + 18 36 const query = server.db.prepare<unknown[], ComAtprotoLabelDefs.Label>(`SELECT * FROM labels WHERE uri = ?`).all(did); 19 37 20 38 const labels = query.reduce((set, label) => { ··· 24 42 }, new Set<string>()); 25 43 26 44 if (rkey.includes(DELETE)) { 27 - await server 28 - .createLabels({ uri: did }, { negate: [...labels] }) 29 - .catch((err) => { 30 - console.log(err); 31 - }) 32 - .then(() => console.log(`Deleted labels for ${did}`)); 45 + await handleDeleteLabels(did, labels); 46 + } else if (labels.size === 0) { 47 + await handleAddLabel(did); 33 48 } else { 34 - const randomHouse = HOUSES[Math.floor(Math.random() * HOUSES.length)]; 35 - await server 36 - .createLabel({ uri: did, val: randomHouse }) 37 - .catch((err) => { 38 - console.log(err); 39 - }) 40 - .then(() => console.log(`Labeled ${did} with ${randomHouse}`)); 49 + console.log(`${did} already has a label. No action taken.`); 41 50 } 42 51 }; 52 + 53 + async function canPerformLabelOperation(did: string): Promise<boolean> { 54 + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 55 + const query = server.db.prepare<unknown[], { count: number }>( 56 + `SELECT COUNT(*) as count FROM labels WHERE uri = ? AND cts > ?` 57 + ).get(did, thirtyDaysAgo.toISOString())!; 58 + 59 + return query.count < 2; 60 + } 61 + 62 + async function handleDeleteLabels(did: string, labels: Set<string>) { 63 + try { 64 + if (labels.size > 0 && await canPerformLabelOperation(did)) { 65 + await server.createLabels({ uri: did }, { negate: [...labels] }); 66 + console.log(`Deleted labels for ${did}`); 67 + } else if (labels.size === 0) { 68 + console.log(`No labels to delete for ${did}`); 69 + } else { 70 + console.log(`Cannot delete labels for ${did}: 30-day limit reached`); 71 + } 72 + } catch (err) { 73 + console.error(`Error deleting labels for ${did}:`, err); 74 + } 75 + } 76 + 77 + async function handleAddLabel(did: string) { 78 + try { 79 + if (!await canPerformLabelOperation(did)) { 80 + console.log(`Cannot add label for ${did}: 30-day limit reached`); 81 + return; 82 + } 83 + 84 + const { data } = await agent.getProfile({ actor: did }); 85 + if (!data) throw new Error('Profile not found'); 86 + 87 + const avatar = await prepareAvatar(data); 88 + const prompt = createPrompt(data); 89 + 90 + await generateText({ 91 + model: openai('gpt-4o'), 92 + messages: [ 93 + { 94 + role: 'user', 95 + content: [ 96 + { type: 'text', text: prompt }, 97 + { 98 + type: 'image', 99 + image: avatar.toBuffer(), 100 + experimental_providerMetadata: { openai: { imageDetail: 'low' } }, 101 + }, 102 + ], 103 + }, 104 + ], 105 + toolChoice: 'required', 106 + tools: { 107 + decide: tool({ 108 + parameters: z.object({ 109 + answer: z.union([ 110 + z.literal('gryffindor'), 111 + z.literal('hufflepuff'), 112 + z.literal('ravenclaw'), 113 + z.literal('slytherin'), 114 + ]), 115 + }), 116 + execute: async ({ answer }) => { 117 + await server.createLabel({ uri: did, val: answer }); 118 + console.log(`Labeled ${did} with ${answer}`); 119 + }, 120 + }), 121 + }, 122 + }); 123 + } catch (err) { 124 + console.error(`Error adding label for ${did}:`, err); 125 + } 126 + } 127 + 128 + async function prepareAvatar(subject: AppBskyActorDefs.ProfileView) { 129 + const size = 100; 130 + const canvas = createCanvas(size, size); 131 + const ctx = canvas.getContext('2d'); 132 + 133 + if (subject.avatar) { 134 + const image = await loadImage(subject.avatar); 135 + ctx.drawImage(image, 0, 0, size, size); 136 + } else { 137 + console.log('No avatar found, using 1x1 white pixel'); 138 + ctx.fillStyle = 'white'; 139 + ctx.fillRect(0, 0, 1, 1); 140 + } 141 + 142 + const avatar = `avatars/${subject.did}.png`; 143 + await fs.writeFile(avatar, canvas.toBuffer()); 144 + 145 + return canvas; 146 + } 147 + 148 + function createPrompt(subject: AppBskyActorDefs.ProfileView) { 149 + return ` 150 + 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? 151 + 152 + 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. 153 + Always return an answer — house name only, all lowercase. 154 + The user's data may be in any language. Focus on the meaning, not just the surface content. 155 + Consider traits for all houses, not just intellect. 156 + 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. 157 + 158 + The user's data is as follows: 159 + 160 + Name: ${subject.displayName || subject.handle} (@${subject.handle}) 161 + Bio: ${subject.description || 'User has no bio.'} 162 + `; 163 + }