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