diff --git a/bun.lock b/bun.lock index 6580c1d..ae7e23e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,20 +5,20 @@ "name": "bsky-echo", "dependencies": { "@atproto/syntax": "^0.4.0", - "@google/genai": "^1.10.0", + "@google/genai": "^1.11.0", "@skyware/bot": "^0.3.12", "@types/js-yaml": "^4.0.9", "consola": "^3.4.2", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.4", "js-yaml": "^4.1.0", - "zod": "^4.0.5", + "zod": "^4.0.14", }, "devDependencies": { "@types/bun": "^1.2.19", "drizzle-kit": "^0.31.4", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.8.3", }, }, }, diff --git a/drizzle/0003_flowery_korvac.sql b/drizzle/0003_flowery_korvac.sql new file mode 100644 index 0000000..d9fe9ba --- /dev/null +++ b/drizzle/0003_flowery_korvac.sql @@ -0,0 +1,3 @@ +ALTER TABLE `interactions` ADD `post` text;--> statement-breakpoint +ALTER TABLE `interactions` ADD `response` text;--> statement-breakpoint +ALTER TABLE `interactions` ADD `muted` integer; \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..21cce7e --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,255 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "30d38111-8e11-4d7d-99e8-cbafd962ca62", + "prevId": "11e8b31f-8e38-4013-8d50-bec6177b015a", + "tables": { + "interactions": { + "name": "interactions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "post": { + "name": "post", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "muted": { + "name": "muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "interactions_uri_unique": { + "name": "interactions_uri_unique", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "memory_block_entries": { + "name": "memory_block_entries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "block_id": { + "name": "block_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_block_entries_block_id_memory_blocks_id_fk": { + "name": "memory_block_entries_block_id_memory_blocks_id_fk", + "tableFrom": "memory_block_entries", + "tableTo": "memory_blocks", + "columnsFrom": [ + "block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "memory_blocks": { + "name": "memory_blocks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'memory'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'User memory'" + }, + "mutable": { + "name": "mutable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "muted_threads": { + "name": "muted_threads", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "muted_at": { + "name": "muted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "muted_threads_uri_unique": { + "name": "muted_threads_uri_unique", + "columns": [ + "uri" + ], + "isUnique": true + }, + "muted_threads_rkey_unique": { + "name": "muted_threads_rkey_unique", + "columns": [ + "rkey" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e6a1257..bb4556e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1753682242260, "tag": "0002_green_millenium_guard", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1754166687687, + "tag": "0003_flowery_korvac", + "breakpoints": true } ] } \ No newline at end of file diff --git a/sqlite.db b/sqlite.db new file mode 100644 index 0000000..de6eb41 Binary files /dev/null and b/sqlite.db differ diff --git a/src/db/index.ts b/src/db/index.ts index 0e06dfd..e044fb1 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,6 +1,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite"; import { Database } from "bun:sqlite"; +import * as schema from "./schema"; import { env } from "../env"; const sqlite = new Database(env.DB_PATH); -export default drizzle(sqlite); +export default drizzle(sqlite, { schema }); diff --git a/src/db/schema.ts b/src/db/schema.ts index 4084e24..3909002 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -5,6 +5,9 @@ export const interactions = sqliteTable("interactions", { id: integer().primaryKey({ autoIncrement: true }), uri: text().unique(), did: text(), + post: text(), + response: text(), + muted: integer({ mode: "boolean" }), created_at: integer({ mode: "timestamp" }).default(sql`CURRENT_TIMESTAMP`), }); diff --git a/src/handlers/posts.ts b/src/handlers/posts.ts index 933cba2..1ea14e7 100644 --- a/src/handlers/posts.ts +++ b/src/handlers/posts.ts @@ -1,4 +1,8 @@ -import { isAuthorizedUser, logInteraction } from "../utils/interactions"; +import { + isAuthorizedUser, + logInteraction, + getRecentInteractions, +} from "../utils/interactions"; import * as threadUtils from "../utils/thread"; import modelPrompt from "../model/prompt.txt"; import { GoogleGenAI } from "@google/genai"; @@ -131,10 +135,13 @@ export async function handler(post: Post): Promise { return; } - await logInteraction(post); - - if (await threadUtils.isThreadMuted(post)) { + const isMuted = await threadUtils.isThreadMuted(post); + if (isMuted) { logger.warn("Thread is muted."); + await logInteraction(post, { + responseText: null, + wasMuted: true, + }); return; } @@ -151,11 +158,17 @@ export async function handler(post: Post): Promise { await MemoryHandler.getBlocks(post.author.did), ); + const recentInteractions = await getRecentInteractions( + post.author.did, + thread, + ); + const memory = yaml.dump({ users_with_memory_blocks: { [env.HANDLE]: botMemory.parseBlocks(), [post.author.handle]: userMemory.parseBlocks(), }, + recent_interactions: recentInteractions, }); logger.log("Parsed memory blocks: ", memory); @@ -167,6 +180,11 @@ export async function handler(post: Post): Promise { if (responseText) { await sendResponse(post, responseText); } + + await logInteraction(post, { + responseText: responseText ?? null, + wasMuted: false, + }); } catch (error) { logger.error("Error in post handler:", error); diff --git a/src/utils/interactions.ts b/src/utils/interactions.ts index 16d61bb..31a78b3 100644 --- a/src/utils/interactions.ts +++ b/src/utils/interactions.ts @@ -1,5 +1,6 @@ import { interactions } from "../db/schema"; import type { Post } from "@skyware/bot"; +import { desc, notInArray } from "drizzle-orm"; import { env } from "../env"; import db from "../db"; @@ -9,11 +10,41 @@ export function isAuthorizedUser(did: string) { : env.AUTHORIZED_USERS.includes(did as any); } -export async function logInteraction(post: Post): Promise { - await db.insert(interactions).values([{ - uri: post.uri, - did: post.author.did, - }]); +export async function logInteraction( + post: Post, + options: { + responseText: string | null; + wasMuted: boolean; + }, +): Promise { + await db.insert(interactions).values([ + { + uri: post.uri, + did: post.author.did, + post: post.text, + response: options.responseText, + muted: options.wasMuted, + }, + ]); - console.log(`Logged interaction, initiated by @${post.author.handle}`); + console.log(`Logged interaction, initiated by @${post.author.handle}`); +} + +export async function getRecentInteractions(did: string, thread: Post[]) { + const threadUris = thread.map((p) => p.uri); + + const recentInteractions = await db.query.interactions.findMany({ + where: (interactions, { eq, and, notInArray }) => + and( + eq(interactions.did, did), + notInArray(interactions.uri, threadUris), + ), + orderBy: (interactions, { desc }) => [desc(interactions.created_at)], + limit: 5, + }); + + return recentInteractions.map((i) => ({ + post: i.post, + response: i.response, + })); }