···66# SPDX-License-Identifier: AGPL-3.0-only
77#
8899-# Collect staged files
1010-STAGED=$(git diff --cached --name-only)
1111-1212-# Track exit status
1313-FAILED=0
99+test() {
1010+ cd backend || return 1
1111+ pnpm run fmt
1212+ pnpm run lint
1313+ cd ../frontend || return 1
1414+ pnpm run fmt
1515+ cd ../lexicons || return 1
1616+ pnpm run generate
1717+ pnpm run prepublish
1818+}
14191520echo "Testing code for any errors before committing..."
1616-1717-run_cmds() {
1818- desc=$1
1919- shift
2020- cd "$desc" || FAILED=1 && return
2121- echo "Running pre-commit checks for $desc..."
2222- if ! "$@"; then
2323- echo "ERROR: Pre-commit checks for $desc failed. Aborting commit."
2424- FAILED=1
2525- fi
2626- cd ..
2121+test
2222+if [ $? -ne 0 ]; then {
2323+ echo "Linting failed. Commit aborted."
2424+ exit 1
2725}
2828-2929-echo "$STAGED" | grep -q "^backend/" && run_cmds "backend" pnpm run fmt && run_cmds "backend" pnpm run lint --fix
3030-git update-index --again
3131-3232-echo "$STAGED" | grep -q "^frontend/" && run_cmds "frontend" pnpm run fmt
3333-git update-index --again
3434-3535-echo "$STAGED" | grep -q "^lexicons/" && run_cmds "lexicons" pnpm run generate && run_cmds "lexicons" pnpm run prepublish
3636-git update-index --again
3737-3838-# If any failed, block commit
3939-if [ $FAILED -ne 0 ]; then
4040- echo "Commit aborted due to failed checks. Please fix your code before committing."
4141- exit 1
4226fi
43274428echo "All relevant checks passed. Proceeding with commit."
+80-20
backend/src/network/commit.ts
···1818import { validateClip, validateProfile, validateTag } from "./validator.js";
1919import { convertDidToString } from "./converters.js";
2020import { hashString } from "../hasher.js";
2121-import { eq } from "drizzle-orm";
2121+import { and, eq } from "drizzle-orm";
2222+import type { TagRef } from "../api/types.js";
22232324const db = Database.getInstance().getDb();
2425···3031export async function handleClip(
3132 event: CommitEvent<`social.clippr.${string}`>,
3233): Promise<void> {
3333- if (event.commit.operation !== "create") {
3434- Logger.warn(
3535- `Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
3636- );
3434+ if (event.commit.operation === "delete") {
3535+ await db
3636+ .delete(clipsTable)
3737+ .where(
3838+ and(
3939+ eq(clipsTable.did, event.did),
4040+ eq(clipsTable.recordKey, event.commit.rkey),
4141+ ),
4242+ );
4343+ Logger.verbose(`Deleted clip: ${event.did}/${event.commit.rkey}`, event);
3744 return;
3838- } // We currently do not handle these.
4545+ }
39464047 if (event.commit.record.$type !== "social.clippr.feed.clip") {
4148 Logger.verbose(
···7683 return;
7784 }
78857979- if (!(await validateClip(record))) {
8686+ if (!(await validateClip(record))) return;
8787+8888+ if (event.commit.operation === "update") {
8989+ await db
9090+ .update(clipsTable)
9191+ .set({
9292+ did: convertDidToString(event.did),
9393+ cid: event.commit.cid,
9494+ timestamp: convertMicroToDate(event.time_us),
9595+ recordKey: event.commit.rkey,
9696+ createdAt: new Date(record.createdAt),
9797+ indexedAt: new Date(),
9898+ url: record.url,
9999+ title: record.title,
100100+ description: record.description,
101101+ tags: record.tags as TagRef[] | undefined,
102102+ notes: record.notes,
103103+ unlisted: record.unlisted,
104104+ unread: record.unread,
105105+ languages: record.languages,
106106+ })
107107+ .where(
108108+ and(
109109+ eq(clipsTable.did, event.did),
110110+ eq(clipsTable.recordKey, event.commit.rkey),
111111+ ),
112112+ );
113113+ Logger.verbose(`Updated clip: ${event.did}/${event.commit.rkey}`, event);
80114 return;
81115 }
82116···104138export async function handleTag(
105139 event: CommitEvent<`social.clippr.${string}`>,
106140): Promise<void> {
107107- if (event.commit.operation !== "create") {
108108- Logger.warn(
109109- `Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
110110- );
141141+ if (event.commit.operation === "delete") {
142142+ await db
143143+ .delete(tagsTable)
144144+ .where(
145145+ and(
146146+ eq(tagsTable.did, event.did),
147147+ eq(tagsTable.recordKey, event.commit.rkey),
148148+ ),
149149+ );
150150+ Logger.verbose(`Deleted tag: ${event.did}/${event.commit.rkey}`, event);
111151 return;
112112- } // We currently do not handle these.
152152+ }
113153114154 if (event.commit.record.$type !== "social.clippr.feed.tag") {
115155 Logger.verbose(
···148188 return;
149189 }
150190191191+ if (event.commit.operation === "update") {
192192+ await db
193193+ .update(tagsTable)
194194+ .set({
195195+ timestamp: convertMicroToDate(event.time_us),
196196+ did: convertDidToString(event.did),
197197+ cid: event.commit.cid,
198198+ recordKey: event.commit.rkey,
199199+ name: record.name,
200200+ description: record.description,
201201+ color: record.color,
202202+ createdAt: new Date(record.createdAt),
203203+ indexedAt: new Date(),
204204+ })
205205+ .where(
206206+ and(
207207+ eq(tagsTable.did, event.did),
208208+ eq(tagsTable.recordKey, event.commit.rkey),
209209+ ),
210210+ );
211211+ Logger.verbose(`Updated tag: ${event.did}/${event.commit.rkey}`, event);
212212+ return;
213213+ }
214214+151215 await db.insert(tagsTable).values({
152216 timestamp: convertMicroToDate(event.time_us),
153217 did: convertDidToString(event.did),
···167231 event: CommitEvent<`social.clippr.${string}`>,
168232): Promise<void> {
169233 if (event.commit.operation === "delete") {
170170- Logger.warn(
171171- `Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
172172- );
234234+ await db.delete(usersTable).where(eq(usersTable.did, event.did));
235235+ Logger.verbose(`Deleted profile: ${event.did}`, event);
173236 return;
174174- } // We currently do not handle deletes.
237237+ }
175238176239 if (event.commit.record.$type !== "social.clippr.actor.profile") {
177240 Logger.verbose(
···257320 avatar: record.avatar?.ref.$link,
258321 description: record.description,
259322 })
260260- .where(eq(usersTable.did, convertDidToString(event.did)))
261261- .execute();
262262-323323+ .where(eq(usersTable.did, convertDidToString(event.did)));
263324 Logger.verbose(`Updated profile: ${convertDidToString(event.did)}`, event);
264264-265325 return;
266326 }
267327
+9-7
frontend/README.md
···4455## development
6677-If you are testing the frontend in conjunction with the AppView, you might want to change the following:
77+If you are testing the frontend in conjunction with the AppView, you might want to change the
88+following:
8999-* OAuth automatically adapts to whether the frontend is built or in dev mode.
1010-* ``VITE_CLIPPR_APPVIEW`` is set to the defaults for both production and development, however, if you are hosting the
1111- appview from another location, you will need to change this.
1010+- OAuth automatically adapts to whether the frontend is built or in dev mode.
1111+- `VITE_CLIPPR_APPVIEW` is set to the defaults for both production and development, however, if you
1212+ are hosting the appview from another location, you will need to change this.
12131314```shell
1415pnpm install
···17181819## deployment
19202020-If you plan to deploy the frontend and use another AppView or to add/remove OAuth scopes, you will have to modify
2121-``public/oauth/client-metadata.json`` and the ``VITE_CLIPPR_APPVIEW`` environment variable. There are plans to add a way
2222-to change what AppView DID the frontend proxies its requests to inside the frontend, but not before launch.
2121+If you plan to deploy the frontend and use another AppView or to add/remove OAuth scopes, you will
2222+have to modify `public/oauth/client-metadata.json` and the `VITE_CLIPPR_APPVIEW` environment
2323+variable. There are plans to add a way to change what AppView DID the frontend proxies its requests
2424+to inside the frontend, but not before launch.
23252426```shell
2527pnpm run build
···11+/*
22+ * clippr: a social bookmarking service for the AT Protocol
33+ * Copyright (c) 2025 clippr contributors.
44+ * SPDX-License-Identifier: AGPL-3.0-only
55+ */
66+77+import { ServiceProxyOptions } from "@atcute/client";
88+99+// Converts the AppView environment variable into options for the client's server proxy options.
1010+export const createServiceProxy = (): ServiceProxyOptions | undefined => {
1111+ const appviewUrl = import.meta.env.VITE_CLIPPR_APPVIEW;
1212+ if (appviewUrl.includes("localhost:")) return undefined; // TODO: You can't do PDS proxying if you're testing locally!!!
1313+ let sanitizedUrl = appviewUrl.replace(/^(https?:\/\/)/, "did:web:");
1414+1515+ return {
1616+ did: sanitizedUrl as `did:${string}:${string}`,
1717+ serviceId: "#clippr_appview",
1818+ };
1919+};
-6
lexicons/lib/lexicons/index.ts
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71export * as SocialClipprActorDefs from "./types/social/clippr/actor/defs.js";
82export * as SocialClipprActorGetPreferences from "./types/social/clippr/actor/getPreferences.js";
93export * as SocialClipprActorGetProfile from "./types/social/clippr/actor/getProfile.js";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import * as SocialClipprActorDefs from "../actor/defs.js";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";
···11-/*
22- * clippr: a social bookmarking service for the AT Protocol
33- * Copyright (c) 2025 clippr contributors.
44- * SPDX-License-Identifier: AGPL-3.0-only
55- */
66-71import type {} from "@atcute/lexicons";
82import * as v from "@atcute/lexicons/validations";
93import type {} from "@atcute/lexicons/ambient";