/** * 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 { deleteAccount, deleteEntry, deleteFollow, upsertAccount, upsertEntry, upsertFollow } from "@/lib/db/queries"; import { AtUri } from "@atproto/syntax"; import { assureAdminAuth, parseTapEvent } from "@atproto/tap"; import { NextRequest, NextResponse } from "next/server"; import * as com from "@/lexicons/com"; import * as app from "@/lexicons/app"; import { Client } from "@atproto/lex"; const TAP_ADMIN_PASSWORD = process.env.TAP_ADMIN_PASSWORD; function tryParseRecord(record: R, parseFunc: (record: R) => T): T | null { try { return parseFunc(record); } catch { return null; } } export async function POST(request: NextRequest) { if (TAP_ADMIN_PASSWORD) { const authHeader = request.headers.get("Authorization"); if (!authHeader) { return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); } try { assureAdminAuth(TAP_ADMIN_PASSWORD, authHeader); } catch { return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); } } const body = await request.json(); const event = parseTapEvent(body); if (event.type === "identity") { console.log("[tap/webhook] event received (type=identity)"); if (event.status === "deleted") { console.log("[tap/webhook] |> event.status=deleted for did", event.did); await deleteAccount(event.did); } else { // FIXME(sofiaritz.com): Allow customization through env vars const client = new Client("https://public.api.bsky.app"); const profile = await client.call(app.bsky.actor.getProfile, { actor: event.did, }); const updatedAccount = { did: event.did, handle: event.handle, active: event.isActive, avatar: profile.avatar, }; console.log("[tap/webhook] |> event.status!=deleted for did", event.did); console.log("[tap/webhook] |> new account data", updatedAccount); await upsertAccount(updatedAccount); } } if (event.type === "record") { console.log("[tap/webhook] event received (type=record)"); const uri = AtUri.make(event.did, event.collection, event.rkey); if (event.action === "create" || event.action === "update") { console.log("[tap/webhook] |> event.action=create|update for record", uri.toString()); if (event.collection === com.sofiaritz.everest.entry.$type) { const record = tryParseRecord( event.record, com.sofiaritz.everest.entry.$parse, ); if (!record) return NextResponse.json({ success: false }); await upsertEntry({ uri: uri.toString(), authorDid: event.did, website: record.website, note: record.note, tags: record.tags, createdAt: new Date(record.createdAt), indexedAt: new Date(), }); } else if (event.collection === com.sofiaritz.everest.graph.follow.$type) { const record = tryParseRecord( event.record, com.sofiaritz.everest.graph.follow.$parse, ); if (!record) return NextResponse.json({ success: false }); await upsertFollow({ uri: uri.toString(), follower: event.did, following: record.subject, createdAt: new Date(record.createdAt), indexedAt: new Date(), }); } } else if (event.action === "delete") { console.log("[tap/webhook] |> event.action=delete for record", uri.toString()); if (event.collection === com.sofiaritz.everest.entry.$type) { await deleteEntry(uri); } else if (event.collection === com.sofiaritz.everest.graph.follow.$type) { await deleteFollow(uri); } } } return NextResponse.json({ success: true }); }