/** * Copyright (C) 2026 SofĂ­a Aritz and contributors * * This file is part of Everest. * * Everest is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * Everest is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Everest. If not, see . */ import { AccountTable, EntryTable, FollowTable, getDb } from "@/lib/db"; import { AtIdentifierString, AtUri } from "@atproto/syntax"; import { Client } from "@atproto/lex"; import * as app from "@/lexicons/app"; import { sql } from "kysely"; export async function listAccountDids() { const db = getDb(); return db .selectFrom("account") .select("did") .execute(); } export async function getNetworkEntries(subject: string, limit = 10) { const db = getDb(); return db .selectFrom("entry") .innerJoin("account", "entry.authorDid", "account.did") .selectAll("entry") .select(["account.handle", "account.avatar"]) .where((eb) => eb.or([ eb("entry.authorDid", "=", subject), eb("entry.authorDid", "in", db .selectFrom("follow") .select("following") .where("follower", "=", subject) ), ])) .orderBy("entry.createdAt", "desc") .limit(limit) .execute(); } export async function getRandomEntry() { // FIXME(sofiaritz.com): This is real inefficient lol, it might be fine for // <10k rows but the moment it start growing... const db = getDb(); return db .selectFrom("entry") .selectAll() .orderBy(sql`random()`) .limit(1) .executeTakeFirst(); } export async function getRecentEntries(limit = 10) { const db = getDb(); return db .selectFrom("entry") .innerJoin("account", "entry.authorDid", "account.did") .selectAll() .orderBy("createdAt", "desc") .limit(limit) .execute(); } export async function getAccountHandle(did: string) { const db = getDb(); const account = await db .selectFrom("account") .select("handle") .where("did", "=", did) .executeTakeFirst(); if (account) return account.handle; try { const client = new Client("https://public.api.bsky.app"); const profile = await client.call(app.bsky.actor.getProfile, { actor: did as AtIdentifierString, }); return profile.handle; } catch { return null; } } export async function getAccountAvatar(did: string) { const db = getDb(); const account = await db .selectFrom("account") .select("avatar") .where("did", "=", did) .executeTakeFirst(); if (account && account.avatar) return account.avatar; try { const client = new Client("https://public.api.bsky.app"); const profile = await client.call(app.bsky.actor.getProfile, { actor: did as AtIdentifierString, }); return profile.avatar ?? null; } catch { return null; } } export async function getAccountEntries(did: string, start = 0, limit: number | null = null) { const db = getDb(); return await db .selectFrom("entry") .selectAll() .where("authorDid", "=", did) .orderBy("createdAt", "desc") .offset(start) .limit(limit) .execute(); } export async function getEntry(uri: AtUri) { const db = getDb(); const entry = await db .selectFrom("entry") .selectAll() .where("uri", "=", uri.toString()) .limit(1) .executeTakeFirst(); return entry ?? null; } export async function upsertEntry(data: EntryTable) { await getDb() .insertInto("entry") .values(data) .onConflict((oc) => oc.column("uri").doUpdateSet({ website: data.website, note: data.note, tags: data.tags, createdAt: data.createdAt, indexedAt: data.indexedAt, })) .execute(); } export async function deleteEntry(uri: AtUri) { await getDb().deleteFrom("entry").where("uri", "=", uri.toString()).execute(); } export async function getFollowing(did: string) { const db = getDb(); return await db .selectFrom("follow") .innerJoin("account", "follow.following", "account.did") .select([ "account.did", "account.avatar", "account.handle", "follow.following" ]) .where("follow.follower", "=", did) .orderBy("follow.createdAt", "desc") .execute(); } export async function getFollowers(did: string) { const db = getDb(); return await db .selectFrom("follow") .innerJoin("account", "follow.follower", "account.did") .select([ "account.did", "account.avatar", "account.handle", "follow.following" ]) .where("follow.following", "=", did) .orderBy("follow.createdAt", "desc") .execute(); } export async function getFollowRelationship(follower: string, following: string) { return await getDb() .selectFrom("follow") .selectAll() .where("follower", "=", follower) .where("following", "=", following) .limit(1) .executeTakeFirst(); } export async function upsertFollow(data: FollowTable) { await getDb() .insertInto("follow") .values(data) .onConflict((oc) => oc.column("uri").doUpdateSet({ follower: data.follower, following: data.following, createdAt: data.createdAt, indexedAt: data.indexedAt, })) .execute() } export async function deleteFollow(uri: AtUri) { await getDb().deleteFrom("follow").where("uri", "=", uri.toString()).execute(); } export async function upsertAccount(data: AccountTable) { await getDb() .insertInto("account") .values(data) .onConflict((oc) => oc.column("did").doUpdateSet({ handle: data.handle, active: data.active, })) .execute(); } export async function deleteAccount(did: string) { await getDb().deleteFrom("entry").where("authorDid", "=", did).execute(); await getDb().deleteFrom("account").where("did", "=", did).execute(); }